安卓BLE蓝牙

1、简介

image.png
image.png
  • BLE是Bluetooth Low Energy的缩写,又叫蓝牙4.0,区别于蓝牙3.0和之前的技术

  • 主要特点是快速搜索,快速连接,超低功耗保持连接和数据传输,缺点:数据传输速率低,由于其具有低功耗特点,所以经常用在可穿戴设备之中

  • BLE设备分单模和双模两种,双模简称BR,商标为Bluetooth Smart Ready,单模简称BLE或者LE,商标为Bluetooth Smart,双模兼容传统蓝牙,可以和传统蓝牙通信,也可以和BLE通信,单模只能和BR和单模的设备通信,不能和传统蓝牙通信,由于功耗低,待机长,所以常用在手环的智能设备上

  • Android是在4.3后才支持BLE,这说明不是所有蓝牙手机都支持BLE,而且支持BLE的蓝牙手机一般是双模的

2、连接流程

  • 1、获取BluetoothManager对象或者是通过BluetoothAdapter的对象,两个差多不
    (BluetoothManager) getContext().getSystemService(Context.BLUETOOTH_SERVICE); BluetoothAdapter adapter = getBluetoothManager().getAdapter();
    BluetoothAdapter.getDefaultAdapter()
蓝牙是夸进程通信的,所以源码使用了aidl来解决跨进程通信
public static synchronized BluetoothAdapter getDefaultAdapter() {
    if (sAdapter == null) {
        IBinder b = ServiceManager.getService(BLUETOOTH_MANAGER_SERVICE);
        if (b != null) {
            IBluetoothManager managerService = IBluetoothManager.Stub.asInterface(b);
            sAdapter = new BluetoothAdapter(managerService);
        } else {
            Log.e(TAG, "Bluetooth binder is null");
        }
    }
    return sAdapter;
}

  • 2、开始扫描BLE设备,两种方式
    1.使用 adapter.startLeScan(mScanCallback) 这个方法不建议使用,回调在ui线程,不要做耗时操作
adapter.startLeScan(mScanCallback)
  private final BluetoothAdapter.LeScanCallback mScanCallback = new BluetoothAdapter.LeScanCallback() {
        /**
         * ui线程,不要做耗时操作
         * @param device
         * @param rssi 信号强度
         * @param scanRecord 广播数据,解析广播
         */
        @Override
        public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
            ScanResult result = new ScanResult(device, rssi, scanRecord);
            if (TextUtils.isEmpty(device.getName())){

            }else if (!mScanResults.containsKey(device.getAddress())) {
                mScanResults.put(device.getAddress(), result);
                Log.v(TAG, String.format("onLeScan: mac = %s, name = %s, record = (%s)%d",
                        device.getAddress(), device.getName(), byteToString(scanRecord), scanRecord.length));
                mAdapter.refresh(mScanResults);
            }
        }
    };

2、通过BluetoothLeScanner,目前项目使用这个


   BluetoothAdapter adapter = getBluetoothManager().getAdapter();
        BluetoothLeScanner bluetoothLeScanner = adapter.getBluetoothLeScanner();
        bluetoothLeScanner.startScan(new ScanCallback() {
            @Override
            public void onScanResult(int callbackType, ScanResult result) {
                super.onScanResult(callbackType, result);
            }

            @Override
            public void onBatchScanResults(List<ScanResult> results) {
                super.onBatchScanResults(results);
            }

            @Override
            public void onScanFailed(int errorCode) {
                super.onScanFailed(errorCode);
            }
        });
        
  • 3、 BluetoothAdapter.LeScanCallback回调,获取到了 BluetoothDevice 了,然后在获取到GATT句柄

  • 4、获取到GATT句柄 BluetoothGatt,回调都在binder线程池,不能刷新ui,回调在哪个线程都不确定,建议所有的回调post到统一的线程,因为多线程同步有点问题哦,难搞的很

  mBluetoothGatt = mDevice.connectGatt(this, false, new BluetoothGattCallback() {
            @Override
            public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
                Log.v(TAG, String.format("onConnectionStateChange: status = %d, newState = %d", status, newState));

                if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothGatt.STATE_CONNECTED) {
                    onConnected();

                    if (!mBluetoothGatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)) {
                        disconnect();
                    }

                    if (!mBluetoothGatt.discoverServices()) {
                        disconnect();
                    }
                } else {
                    disconnect();
                }
            }

            @Override
            public void onServicesDiscovered(BluetoothGatt gatt, int status) {
                Log.v(TAG, String.format("onServicesDiscovered: status = %d", status));

                if (status == BluetoothGatt.GATT_SUCCESS) {
                    refreshProfile();

                    startChannel();

                    BluetoothUtils.setCharacteristicNotification(gatt, UUID_MYSERVICE, UUID_PACKET, true);
                    mBtnPacket.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            mBtnPacket.setEnabled(true);
                        }
                    }, 1000);
                } else {
                    disconnect();
                }
            }

            @Override
            public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
                Log.v(TAG, String.format("onCharacteristicRead: service = %s, character = %s, value = %s, status = %d",
                        characteristic.getService().getUuid(),
                        characteristic.getUuid(),
                        ByteUtils.byteToString(characteristic.getValue()),
                        status));
            }

            @Override
            public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
                Log.v(TAG, String.format("onCharacteristicWrite service = %s, character = %s, status = %d",
                        characteristic.getService().getUuid(),
                        characteristic.getUuid(),
                        status));
                if (mChannelCallback != null) {
                    mChannelCallback.onCallback(status == BluetoothGatt.GATT_SUCCESS ? Code.SUCCESS : Code.FAIL);
                    mChannelCallback = null;
                }
            }

            @Override
            public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
                Log.v(TAG, String.format("onCharacteristicChanged service = %s, character = %s, value = %s",
                        characteristic.getService().getUuid(),
                        characteristic.getUuid(),
                        ByteUtils.byteToString(characteristic.getValue())));

                mChannel.onRead(characteristic.getValue());
            }

            @Override
            public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
                Log.v(TAG, String.format("onReadRemoteRssi rssi = %d, status = %d", rssi, status));
            }

            @Override
            public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
                Log.v(TAG, String.format("onDescriptorWrite service = %s, character = %s, descriptor = %s, status = %d",
                        descriptor.getCharacteristic().getService().getUuid(),
                        descriptor.getCharacteristic().getUuid(),
                        descriptor.getUuid(), status));
            }
        });
  • log可以看出这里是在binder线程池,每次连接都是一个独立的对象,也在一个独立的线程中,所以每次断开连接都要把GATT关闭掉,而不是 disconnect();
    image.png
  • 通过gatt= mDevice.connectGatt(this, false, new BluetoothGattCallback()出来的对象,其实是和public void onServicesDiscovered(BluetoothGatt gatt, int status)是同一个对象,这个很有意思?具体我也不清楚为啥
image.png
  • 注意事项
  • GATT重连,BluetoothGatt 调用connet函数,但是不建议这样做,而是重新开启一个gatt, 而不是用旧的gatt重连,mBluetoothGatt.connect(); 就是不用使用内部的方法区进行重连?why?因为这里面有好多的坑
  • 断开连接 mBluetoothGatt.close(); 或者使用 disconnect();其实最好是使用close这个方法,这是稳定性最好的办法, disconnect(); 有些很奇怪的问题,能力不够,不要去采坑,close后,就不收到回调了,不用等待回调
  • BluetoothGattCallback接口回调:onCharacteristicChanged 回调中读取蓝牙设备给我们发送的数据

@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
    Log.v(TAG, String.format("onCharacteristicChanged service = %s, character = %s, value = %s",
            characteristic.getService().getUuid(),
            characteristic.getUuid(),
            ByteUtils.byteToString(characteristic.getValue())));

    mChannel.onRead(characteristic.getValue());
}

  • 通过 BluetoothGatt 类并且通过uuid获取到 BluetoothGattService ,在获取到BluetoothGattCharacteristic ,通过这个对象去发送数据, UUID 后面解释
private BluetoothGattCharacteristic getNotifyCharacteristic() {
    if (mBluetoothGatt != null) {
        BluetoothGattService service = mBluetoothGatt.getService(UUID_MYSERVICE);
        return service != null ? service.getCharacteristic(UUID_PACKET) : null;
    }
    return null;
}
BluetoothGattCharacteristic characteristic = getNotifyCharacteristic();
characteristic.setValue(bytes);
characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
mBluetoothGatt.writeCharacteristic(characteristic);
mChannelCallback = callback;

3、什么是GATT

  • 简介:

  • GATT全称Generic Attribute Profile,中文名叫通用属性协议,它定义了services和characteristic两种东西来完成低功耗蓝牙设备之间的数据传输,它是建立在通用数据协议Attribute Protocol (ATT),之上的,ATT把services和characteristic以及相关的数据保存在一张简单的查找表中,该表使用16-bit的id作为索引

  • 一旦两个设备建立了连接,GATT就开始发挥作用

  • GATT连接是独占的,也就意味着一个BLE周边设备同时只能与一个中心设备连接,一旦周边设备与中心设备连接成功,直至连接断开,它不再对外广播自己的存在,其他的设备就无法发现该周边设备的存在了。

  • 周边设备和中心设备要完成双方的通信只能通过建立GATT连接的方式。

  • GATT架构图


    image.png
  • 1、profile可以理解为一种规范,一个标准的通信协议,其存在于手机中,蓝牙组织规定一些标准的profile:HID OVER GATT ,防丢器等,每个profile中包含了多个service了。蓝牙规范(Profile)是指蓝牙通信在那一种用途下应该使用的通信协议和相关的规范。蓝牙1.1定义的profile有13个。SIG认为蓝牙设备有4个最基本的Profile

2、service可以理解为一个服务,在ble从机中,通过有多个服务,例如电量信息服务、系统信息服务等,每个service中又包含多个characteristic特征值。每个具体的characteristic特征值才是ble通信的主题。比如当前的电量是80%,所以会通过电量的characteristic特征值存在从机的profile里,这样主机就可以通过这个characteristic来读取80%这个数据
3、characteristic特征值,ble主从机的通信均是通过characteristic来实现,可以理解为一个标签,通过这个标签可以获取或者写入想要的内容。

把Profile 当做是国家,Service 就是中国足球。Characteristic就是国足的某一个人,我需要找到这个人告诉他,明天不要提假球(类似对ble设备设置信息),所以需要uuid,uuid一般是问询问硬件工程师,问他要这个设备的UUID

  • 主要用的是service UUID 和characteristic UUID一般读,写和通知的UUID 就是 characteristic UUID

4、UUID到底是个啥?

  • 通用唯一识别码(英语:Universally Unique Identifier,缩写:UUID)是用于计算机体系中以识别信息数目的一个128位标识符,还有相关的术语:全局唯一标识符
  • UUID是由一组32位数的16进制数字所构成,故UUID理论上的总数为1632=2128,约等于3.4 x 1038。也就是说若每纳秒(ns)产生1兆个UUID,要花100亿年才会将所有UUID用完
  • UUID 的 16 个 8 位字节表示为 32 个十六进制(基数16)数字,显示在由连字符分隔 '-' 的五个组中,"8-4-4-4-12" 总共 36 个字符(32 个字母数字字符和 4 个连字符)。例如:123e4567-e89b-12d3-a456-426655440000

5、BLE一些问题的总结,刚入坑不久,持续记录

  • 经典蓝牙是通过socket ,BLE是通过gatt
  • BLE分Service /Characteristic 。一个地址,Service相当于省,Characteristic相当于市
  • 我觉得未来物联网发展到繁华的时候:最靠谱的设备识别方式是解析广播内容,其中有个设备id,然后拿着这个id,去云端查询设备信息,然后配对。但是我目前是靠名字配对的
  • GATT 句柄泄漏,不用了就把GATTclose掉,不关的话,就会重新开GATT,导致系统句柄耗尽,导致没有办法创建新的连接
  • 重复连接,打开一个gatt句柄,就会创建一个通道,这个通道是独立的
  • 在高版本的手机上,在发现新的设备的过程,还需要运行时的权限,切记 (Android M 以上)
  • gatt的回调是binder线程池,如何把结果的数据统一的管理,这个问题我想的解决方式动态代理去放到统一的线程,所以我现在在做这样的工作
  • and so on
安卓BLE蓝牙.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容