安卓低功耗蓝牙的使用

近期一个项目需要用到低功耗蓝牙的开发,由于之前没有蓝牙开发的经验,发现网上关于蓝牙开发的资料不多,不是随便描述一下就是已经过时的,在此整理一篇低功耗蓝牙的入门资料,能够完成使用蓝牙的接受和发送数据。

低功耗蓝牙 (BLE,Bluetooth Low Energy的简称) 从Android 4.3 开始支持,如今越来越多外设都是使用低功耗蓝牙来传输数据的,与经典蓝牙本质上没有太多的区别,有很多相似之处,工作流程都是:发现设备 --> 配对/绑定设备 --> 连接设备 --> 数据传输。但是,低功耗蓝牙在安卓开发中的使用和经典蓝牙是完全不同的,如果按照之前很熟悉的经典蓝牙开发思维来做,说不定还会踩坑。。。

官方相关的开发指南:
经典蓝牙
低功耗蓝牙
低功耗蓝牙使用实例项目

基本概念

先来了解一些关于低功耗蓝牙的基本概念:

  • Generic Attribute Profile (GATT)——全称叫做通用属性配置文件,GATT按照层级定义了三个概念,服务(Service)、特征(Characteristic)和描述(Descriptor)。一个 Service 包含若干个 Characteristic,一个 Characteristic 包含若干个 Descriptor。
  • Characteristic——可以理解为一个类,包含了一个 value 和零至多个对该 value 的描述。
  • Descriptor——对 Characteristic 的描述,例如范围和计量单位等。
  • Service——Characteristic的集合。

这些概念不用深入去探究,有一定了解开发的时候不至于一无所知就够了,想要具体了解低功耗蓝牙这里有篇不错的文章

低功耗蓝牙开发步骤

1.声明权限

使用蓝牙功能首先需要声明相关的权限,比如:

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

同时,也就可以通过蓝牙特性配置来限制支持蓝牙功能的设备使用APP:

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

或者通过在代码中判断,不过现在基本没有什么手机不支持蓝牙功能了吧。

// Use this check to determine whether BLE is supported on the device. Then
// you can selectively disable BLE-related features.
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
    Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show();
    finish();
}

需要注意的是,官方说明 Android 5.0 及以上设备使用蓝牙时还需要定位权限,需要注意的是开发的时候如果是在 Android 6.0 及以上设备的需要动态获取定位权限,否则蓝牙功能也是无法使用的。

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> 
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<manifest ... >
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    ...
    <!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
    <uses-feature android:name="android.hardware.location.gps" />
    ...
</manifest>
2.初始化蓝牙适配器
private BluetoothAdapter mBluetoothAdapter;
...
// Initializes Bluetooth adapter.
final BluetoothManager bluetoothManager =
        (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();
3.开启蓝牙

在开始扫描发现蓝牙设备之前需要确保手机的蓝牙功能打开。

if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    // 申请打开蓝牙
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

然后在 onActivityResultI方法中判断用户是否同意开启蓝牙功能。

4.发现设备
private static final long SCAN_PERIOD = 10000;
private void scanLeDevice(final boolean enable) {
      // Stops scanning after a pre-defined scan period.
      mHandler.postDelayed(new Runnable() {
          @Override
          public void run() {
              mScanning = false;
              mBluetoothAdapter.stopLeScan(mLeScanCallback);
          }
      }, SCAN_PERIOD);
      mScanning = true;
      mBluetoothAdapter.startLeScan(mLeScanCallback);
}

/**
* 发现设备的回调
*/
private BluetoothAdapter.LeScanCallback mLeScanCallback =
        new BluetoothAdapter.LeScanCallback() {
    @Override
    public void onLeScan(final BluetoothDevice device, int rssi,
            byte[] scanRecord) {

    }
};
5.连接设备
mBluetoothGatt = device.connectGatt(this, false, mGattCallback);

连接设备的方法需要传入三个参数,第一个是 context,第二个是 boolean,表示是否自动连接,第三个是连接的回调接口,其中有几个很重要的方法。

    private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            String intentAction;
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                mConnectionState = STATE_CONNECTED;
                // 开始查找服务,只有找到服务才算是真的连接上
                mBluetoothGatt.discoverServices();
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                intentAction = ACTION_GATT_DISCONNECTED;
                mConnectionState = STATE_DISCONNECTED;
            }
        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {

            } else {

            }
        }

        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            super.onCharacteristicWrite(gatt, characteristic, status);
        }

        @Override
        public void onCharacteristicRead(BluetoothGatt gatt,
                                         BluetoothGattCharacteristic characteristic,
                                         int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
            }
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
              byte[] data = characteristic.getValue();
        }
    };

连接设备成功后需要在回调方法中发现服务mBluetoothGatt.discoverServices(),发现服务后会回调onServicesDiscovered方法,发现服务成功才算是真正的连接上蓝牙设备。onCharacteristicWrite方法是写操作结果的回调,onCharacteristicChanged方法是状态改变的回调,在该方法中能够获取蓝牙发送的数据。不过,在接收数据之前,我们必须对Characteristic设置监听才能够接收到蓝牙的数据。

6.Characteristic监听设置
public void setNotification() {
 
    BluetoothGattService service = mBluetoothGatt.getService(SERVICE_UUID);
    if (service == null) {
        L.e("未找到蓝牙中的对应服务");
        return;
    }
    BluetoothGattCharacteristic characteristic= service.getCharacteristic(CharacteristicUUID);
    if (characteristic== null) {
        L.e("未找到蓝牙中的对应特征");
        return;
    }
    //设置true为启用通知,false反之
    mBluetoothGatt.setCharacteristicNotification(characteristic, true);
 
    //下面为开启蓝牙notify功能,向CCCD中写入值1
    BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CCCD);
    descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    mBluetoothGatt.writeDescriptor(descriptor);
}

先通过 UUID 找到我们需要进行数据传输的service,在找到我们想要设置监听的Characteristic的 UUID 找到响应的characteristic对象,找到响应的characteristic后调用setCharacteristicNotification方法启用通知,则该characteristic状态发生改变后就会回调onCharacteristicChanged方法,而开启蓝牙的 notify 功能需要向 UUID 为 CCCD 的 descriptor 中写入值1,其中 CCCD 的值为:

public static final UUID CCCD = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
7.向蓝牙发送数据

往蓝牙发送数据,可以理解为给蓝牙的characteristic设置数据。

public void writeRXCharacteristic(byte[] value) {
        if (mBluetoothGatt == null) {
            return;
        }
        BluetoothGattService service= mBluetoothGatt.getService(SERVICE_UUID);
        BluetoothGattCharacteristic characteristic= service.getCharacteristic(UUID);
        characteristic.setValue(value);
        mBluetoothGatt.writeCharacteristic(characteristic);
    }

我们可以写入Stringbyte[]的数据,一般为byte[]。其中我们需要与哪个service的characteristic进行数据传输可以联系硬件工程师或者查看蓝牙设备供应商提供的说明获得。我们也可以通过mBluetoothGatt.getServices()mBluetoothGatt.getgetCharacteristics()方法获取蓝牙设备的所有
service 和某个 service 中的所有 characteristic。

8.数据分包处理

低功耗蓝牙一次性只能发送 20 个字节的数据,超过 20 个字节的无法发送,因此需要对发送的数据进行分包处理,在此给出数据分包的一个示例,是在别人 github 中看到的:

//存储待发送的数据队列
    private Queue<byte[]> dataInfoQueue = new LinkedList<>();
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            send();
        }
    };

    /**
     * 向characteristic写数据
     *
     * @param value
     */
    public void writeCharacteristic(BluetoothGattCharacteristic characteristic, byte[] value) {
        this.mCharacteristic = characteristic;
        if (dataInfoQueue != null) {
            dataInfoQueue.clear();
            dataInfoQueue = splitPacketFor20Byte(value);
            handler.post(runnable);
        }
        // characteristic.setValue(value);
        // mBluetoothGatt.writeCharacteristic(characteristic);
    }

    private void send() {
        if (dataInfoQueue != null && !dataInfoQueue.isEmpty()) {
            //检测到发送数据,直接发送
            if (dataInfoQueue.peek() != null) {
                this.mCharacteristic.setValue(dataInfoQueue.poll());//移除并返回队列头部的元素
                mBluetoothGatt.writeCharacteristic(mCharacteristic);
            }
            //检测还有数据,延时后继续发送,一般延时100毫秒左右
            if (dataInfoQueue.peek() != null) {
                handler.postDelayed(runnable, 200);
            }
        }
    }

    //数据分包处理
    private Queue<byte[]> splitPacketFor20Byte(byte[] data) {
        Queue<byte[]> dataInfoQueue = new LinkedList<>();
        if (data != null) {
            int index = 0;
            do {
                byte[] surplusData = new byte[data.length - index];
                byte[] currentData;
                System.arraycopy(data, index, surplusData, 0, data.length - index);
                if (surplusData.length <= 20) {
                    currentData = new byte[surplusData.length];
                    System.arraycopy(surplusData, 0, currentData, 0, surplusData.length);
                    index += surplusData.length;
                } else {
                    currentData = new byte[20];
                    System.arraycopy(data, index, currentData, 0, 20);
                    index += 20;
                }
                dataInfoQueue.offer(currentData);
            } while (index < data.length);
        }
        return dataInfoQueue;
    }

这篇文章简单介绍了安卓进行低功耗蓝牙开发的流程以及提到了一些注意事项,看完本文基本就能够进行低功耗蓝牙开发,除此以外,强烈推荐去 Github 看一下低功耗蓝牙使用实例项目,例子不难理解,看懂了 Demo 能对低功耗蓝牙的使用有更深的了解。

本文原文地址:Android 低功耗蓝牙开发

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,009评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,808评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,891评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,283评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,285评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,409评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,809评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,487评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,680评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,499评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,548评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,268评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,815评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,872评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,102评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,683评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,253评论 2 341

推荐阅读更多精彩内容

  • BLE 与经典蓝牙的区别 BLE 的 Kotlin 下实践 BluetoothGattCallback 不回调异常...
    chauI阅读 10,686评论 1 7
  • 初识低功耗蓝牙 Android 4.3(API Level 18)开始引入Bluetooth Low Energy...
    JBD阅读 112,432评论 46 342
  • 蓝牙 蓝牙的波段为2400-2483.5MHz(包括防护频带)。这是全球范围内无需取得执照(但定不是无管制的)的工...
    苏永茂阅读 6,089评论 0 11
  • 我愿意回到起点去做自己喜欢的事情。
    萤火虫的梦想阅读 56评论 0 0
  • 即使我已经洞察周易的结果 但也只愿意 和你重新来过 我想把所有的乖戾残忍狰狞疯狂倾洒给世界 因为那样 给你的 就只...
    周荀川阅读 617评论 0 2