Android之低功耗蓝牙的基本使用

本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

前言:

最近在工作中使用到蓝牙的功能,当然我们这个蓝牙时跟蓝牙芯片结合使用的,而不是手机跟手机连接通信的。其实本质时差不多的,只是设备不一样罢了。在这里我不会贴出蓝牙那些协议等等复杂的名词解释,因为这个不是一两句话能解释的清楚,在我们先不太了解蓝牙的这些专业名词之前,我们先掌握它的基本使用就可以了,后续如果想深入了解的话,我们再花时间去学习。本文介绍的低功耗的蓝牙,是Android 4.3才开始支持的,而要使用传统蓝牙和高版本的蓝牙请参照官方文档,有中文介绍哦。


官方蓝牙文档:https://developer.android.google.cn/guide/topics/connectivity/bluetooth.html

官方不同版本蓝牙示例(有3个):

https://github.com/googlesamples?utf8=%E2%9C%93&q=bluetooth&type=&language=

一、蓝牙的基本使用流程(草图)


从图上可以看出我这个例子只是单方面的通信,即手机只接收数据而不发送数据。

二、蓝牙关键类

BluetoothManager:蓝牙管理服务,如果对Android基本框架熟悉的话,你会发现蓝牙也属于最底层的驱动模块里,那么要使用蓝牙的东西就需要使用(BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE)来获取了。不过看是把图贴一下。


BluetoothAdapter:表示本地设备蓝牙适配器。 BluetoothAdapter允许您执行基本的蓝牙任务,例如启动设备发现,查询已绑定(配对)设备的列表,使用已知MAC地址实例化BluetoothDevice,并创建一个BluetoothServerSocket以监听来自其他设备的连接请求,并启动扫描蓝牙LE设备。这个类时关键类,后面会大量使用到它。

BluetoothAdapter.LeScanCallback:用于提供LE扫描结果的回调界面。这个类就是扫描的回调接口,不过再Android 5.0以上使用抽象类ScanCallback。

BluetoothLeScanner:该类提供了对蓝牙LE设备执行扫描相关操作的方法。应用程序可以使用ScanFilter扫描特定类型的蓝牙LE设备。它还可以请求不同类型的回调来传递结果。不过这个类是在Android 5.0(API21)以上才出现的,也就是对于Android 4.3的以上我们只会使用BluetoothAdapter来进行扫描等操作,当然如果是5.0以上的可以使用这个类来代替的。

ScanCallback:蓝牙LE扫描回调,使用这些回调报告扫描结果。这个是抽象类与BluetoothLeScanner配套使用。

BluetoothDevice:表示远程蓝牙设备,BluetoothDevice允许您创建与相应设备的连接或关于它的查询信息,例如名称,地址,类和绑定状态。

BluetoothProfile:配置文件代理。每个公共配置文件实现这个接口。它有几个直接子类,每个子类再不同场景中使用,如BluetoothA2dp, BluetoothGatt, BluetoothGattServer, BluetoothHeadset, BluetoothHealth,另外还有个隐藏子类BluetoothInputDevice(这个可以用在HID的情况下)。在当前例子中使用到的是BluetoothGatt。

BluetoothGatt:该类提供蓝牙GATT功能,以实现与蓝牙智能或智能就绪设备的通信。后续使用该类做连接、断开、关闭等操作。

BluetoothGattCallback:文档没有直接的解释,只说了被用在连接设备时候的回调。虽然文档没有详细说明,但是这个回调会在后续的连接、断开、通信中起到关键作用。

好了,主要的几个类介绍的差不多了。接下来我们使用这几个类练习一下。

1、获取蓝牙管理服务和适配器

bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
bluetoothAdapter = bluetoothManager.getAdapter();
获取了适配器后,我们可以通过适配器是否为null来判断是否支持蓝牙功能(想必现在的手机应该都支持吧)。另外适配器还提供了以下的方法:

/**
 * 启用蓝牙
 */
public boolean enableBluetooth()
{
    if (isSupportBluetooth())
    {
        return bluetoothAdapter.enable();
    } else
    {
        return false;
    }
}

/**
 * 是否启用了
 *
 * @return
 */
public boolean isEnabled()
{
    if (isSupportBluetooth())
    {
        return bluetoothAdapter.isEnabled();
    }
    return false;
}
/**
 * 禁用蓝牙
 */
public boolean disableBluetooth()
{
    if (isSupportBluetooth())
    {
        return bluetoothAdapter.disable();
    } else
    {
        return false;
    }
}
/**
 * 扫描设备
 */
public void scanLeDevice()
{
    if (isSupportBluetooth() && isEnabled())
    {
        setScanning(true);
        getBluetoothAdapter().startLeScan(leScanCallback);
        getHandler().postDelayed(new Runnable()
        {
            @Override
            public void run()
            {
                stopLeScan();
            }
        }, getScanPeriod());
    }
}

/**
 * 停止扫描
 */
public void stopLeScan()
{
    if (isSupportBluetooth() && isEnabled())
    {
        setScanning(false);
        getBluetoothAdapter().stopLeScan(leScanCallback);
    }
}
上面的就是启用、关闭、扫描、停止的几个方法。需要注意的是在启用蓝牙的时候在不同手机会弹出启用对话框,比如魅族。那么这个时候你需要通过isEnabled方法来判断是否启用了,如果没有启用则使用startActivity来启用,然后通过Activity或者Fragment的onActivityResult回调方法来做余下的操作了。

startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE),  REQUEST_ENABLE);

好了,使用上面的扫描方法就可以扫描到蓝牙设备了,是不是很简单啊。再说明一下,上面的leScanCallback就是上面提到的BluetoothAdapter.LeScanCallback,你需要实现它就可以了。

2、权限配置

不过使用蓝牙也是要配置权限的,如果你没有在Manifest中配置,在Android Studio中使用上面的方法的时候它会报错要求你加入权限。我在这里列一下我使用到的权限:

<!-- 需要硬件支持低功耗蓝牙 -->
<uses-feature
    android:name="android.hardware.bluetooth_le"
    android:required="true"/>
<!-- 蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<!-- Android 5.0以上蓝牙还需要位置权限 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
是的,你会发现使用蓝牙还需要位置权限?我开始也觉得一个蓝牙也要位置权限,后来在翻看官方文档的时候才知道的,不然你在5.0以上的手机上扫不出蓝牙的!详情请翻阅官方文档:https://developer.android.google.cn/guide/topics/connectivity/bluetooth-le.html#user-permission

3、连接设备

有了蓝牙设备后接下来我们连接下,然后发发数据看看。连接设备其实也很简单,就是通过扫描到的蓝牙设备对象来连接即可。蓝牙设备就是上面提到的BluetoothDevice,这个类有一个方法:

public BluetoothGatt connectGatt(Context context, boolean autoConnect,
                                     BluetoothGattCallback callback) {
        return (connectGatt(context, autoConnect,callback, TRANSPORT_AUTO));
    }
第一个Context就不说了;第二个参数autoConnect的解释是“是否直接连接到远程设备(false)或一旦远程设备可用即可自动连接(true)”;第三个参数也是上面提到的抽象类,这个类在连接过程中起到关键作用。最后方法会返回一个BluetoothGatt对象,后续我们通过这个对象可以重连、断开、关闭设备。贴下我例子中的连接方法:

/**
 * 连接设备,如果服务未开启或者地址为空的话就返回false;如果地址存在是否连接成功取决与蓝牙底层
 *
 * @param address
 * @return 是否连接到
 */
@Override
public boolean connectDevice(String address)
{
    if (arshowBluetooth.getBluetoothAdapter() == null || address == null)
    {
        return false;
    }
    //如果之前有连接过就直接连接,重新连接
    if (address.equals(mAddress) && mBluetoothGatt != null)
    {
        return mBluetoothGatt.connect();
    }
    BluetoothDevice device = arshowBluetooth.getBluetoothAdapter().getRemoteDevice(address);
    if (device == null)
    {
        return false;
    }
    //false表示直接连接,true表示远程设备可用之后连接
    mBluetoothGatt = device.connectGatt(context, false, mGattCallback);
    mAddress = address;
    return true;
}

抽象类

/**
 * 连接、发现、通信回调
 */
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback()
{
    /**
     * 连接状态改变回调
     * @param gatt
     * @param status
     * @param newState
     */
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState)
    {
        if (newState == BluetoothProfile.STATE_CONNECTED)
        {
            //阅读连接的远程设备的RSSI            gatt.readRemoteRssi();
            // 发现远程设备提供的服务及其特性和描述符
            gatt.discoverServices();
        }
        bluetoothCallback.connectionStateChange(gatt.getDevice(), newState);
    }

    /**
     * 当远程设备的远程服务列表,特征和描述符已被更新,即已发现新服务时,调用回调。表示可以与之通信了。
     * @param gatt
     * @param status
     */
    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status)
    {
        if (status == BluetoothGatt.GATT_SUCCESS)
        {
            // 得到服务对象
            BluetoothGattService service = gatt.getService(BluetoothConstant.UUID_SERVICE);
            if (service == null)
            {
                return;
            }

            // 得到此服务结点下Characteristic对象
            final BluetoothGattCharacteristic gattCharacteristic = service
                    .getCharacteristic(BluetoothConstant.UUID_CHARACTERISTIC);
            if (gattCharacteristic == null)
            {
                return;
            }
            gatt.setCharacteristicNotification(gattCharacteristic, true);
            BluetoothGattDescriptor descriptor = gattCharacteristic
                    .getDescriptor(BluetoothConstant.UUID_DESCRIPTOR);
            descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
            gatt.writeDescriptor(descriptor);

            bluetoothCallback.serviceDiscoveryed(gatt.getDevice(), status);
        } else
        {
            LogUtils.w(getClass().getSimpleName(), "onServicesDiscovered received: " + status);
        }
    }

    /**
     * 由于远程特征通知而触发回调。
     * @param gatt
     * @param characteristic
     */
    @Override
    public void onCharacteristicChanged(BluetoothGatt gatt,
                                        BluetoothGattCharacteristic characteristic)
    {
        try
        {
            bluetoothCallback.valueChanged(new String(characteristic.getValue(), "UTF-8"));
        } catch (UnsupportedEncodingException e)
        {
            e.printStackTrace();
        }
    }

    /**
     * 返回远程设备的信号强度,最大值理论值为0
     * @param gatt
     * @param rssi
     * @param status
     */
    @Override
    public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status)
    {
        super.onReadRemoteRssi(gatt, rssi, status);
    }
};
这个类里面有很多方法,不过都被这个类实现了,我们根据自己的情况来重写。解释下上面列出的方法:

onConnectionStateChange:监测蓝牙设备的连接、断开、读取设备信号强度值、发现服务(翻译的好别扭)。连接和断开可以通过newState参数来判断,注意是和BluetoothProfile的常量值来对比的,这个类也是上面提到过的。其实这个类提供了4个状态值,只不过在使用过程中只有连接(状态值2)和断开(状态值0),其他的没用到。

/** The profile is in disconnected state */
public static final int STATE_DISCONNECTED  = 0;
/** The profile is in connecting state */
public static final int STATE_CONNECTING    = 1;
/** The profile is in connected state */
public static final int STATE_CONNECTED     = 2;
/** The profile is in disconnecting state */
public static final int STATE_DISCONNECTING = 3;
至于后面的读取设备信号强度值、发现服务这两个分别用在获取信号强度和通信的。怎么解释?要知道蓝牙也是无线传输的,既然是无线就跟移动网、WIFI一样都存在信号强弱的情况,那么通过这个就可以获取了。信号强度值是负数,负数越大信号越好(最大理论值是0),单位是dBm。而发现服务,我们在成功连接后调用gatt.discoverServices();方法既可以获取与连接的设备进行通信了,最后回调onServicesDiscovered方法。

onServicesDiscovered:这个方法里面我们需要做的就是使用蓝牙的UUID获取服务、特征、描述等对象,然后就可以给手机发送数据了。这里需要注意的是UUID不能随便写,而是设备厂商或者通用的UUID,否则你是无法使用的。说到这个UUID,其实我也比较迷惑的,很多文章在介绍UUID的时候都是一笔带过或者直接拿来用根本不解释这是干嘛的,而且也没找到一个比较全面介绍蓝牙协议等专业文档(谁找到了,麻烦给我留一个地址)。其实说到这,里面提到的特征、描述等对象,我也没详细去说请原谅。

补充下UUID的说明,UUID就是上面说到的厂商设置通用的,比如00002a29-0000-1000-8000-00805f9b34fb这个就代表生产厂商的UUID,使用它可以获取厂商信息,另外我也收录了一些常见的UUID,可以参考。常见UUID


onCharacteristicChanged:远程特性通知,回调触发。就是上面执行完后,只要连接的设备触发某些动作(比如我的蓝牙芯片按钮按下、抬起),这个方法就会被调用了。然后我们在这个方法中就可以获取远程设备发过来的信息。

好了,其实写到我这个例子基本的使用已经差不多了,剩下的无法就是蓝牙的断开、连接、关闭(释放资源)

/**
 * 断开设备
 */
@Override
public void disconnectDevice()
{
    if (mBluetoothGatt != null)
    {
        mBluetoothGatt.disconnect();
    }
}

@Override
public void closeDevice()
{
    if (mBluetoothGatt != null)
    {
        mBluetoothGatt.close();
    }
}
最后,贴下我的例子截图以及我使用的蓝牙芯片,是不是感觉高大上啊,哈哈~




好了,我这个蓝牙芯片在连接之前的LED指示灯是一闪一闪的,而连接成功后就会常亮的。上图的3个按钮也是对应芯片的按键的。不过在这里说明一下,由于为了保密,我这里不会放出与蓝牙芯片的例子,而是单独写了一个简单的例子,不过底层的东西是不变的。另外这个是单方面的通信交互,如何做到双向通信呢?由于本例子只需要单向通信,所以就没涉及到,不也是上面的例子的范畴,大家可以自己去研究下。另外说明下官方提供的低功耗的例子在使用“发现服务”的那个回调方法存在问题(连接了接收不到数据),我这里跟官方的例子还是不一样的。

另外说明下,我的Android Studio版本是2.4 preview 3版本,对应的gradle版本是gradle-3.4.1-all,所有低版本的自行修改配置。

GitHub地址:https://github.com/Xanthuim/BluetoothSample

最后说明下Android系统的低功耗是在4.3版本才支持的,低版本只能使用传统蓝牙啊。麻烦你们还是先看下谷歌官方文档最好。谷歌蓝牙主要分3个版本4.3以前是传统蓝牙,从4.3开始支持低功耗蓝牙(BLE),从5.0(记得是)又有新的功能了,但是没去研究。

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页