android专题-蓝牙扫描、连接、读写

常用业务API

1.判断当前蓝牙是否已经开启,如果没有开启提示用户开启
2.实时扫描周边蓝牙,获取蓝牙名给用户选择
3.解析蓝牙广播数据处理业务
4.监听外围设备发送给app的数据,处理对应业务
5.app发送数据给外围设备以处理业务

概念

外围设备

可以被其他蓝牙设备连接的外部蓝牙设备,不断广播自身的蓝牙名及其数据,如小米手环、共享单车、蓝牙体重秤

中央设备

可以搜索并连接周边的外围设备,并与之进行数据读写通讯,如手机

日常生活中常见的场景是手机app通过蓝牙开启共享单车,手机app通过蓝牙获取蓝牙体重秤的体重结果,这时候共享单车、蓝牙体重秤就称为外围设备,而手机就称为中央设备

经典蓝牙BT

泛指支持蓝牙协议在4.0以下的模块,一般用于数据量比较大的传输,如:语音、音乐等较高数据量的传输。经典蓝牙模块又可细分为:传统蓝牙和高速蓝牙模块。传统蓝牙模块在2004年推出,主要代表是支持蓝牙2.1协议的模块,在智能手机爆发的时期得到了广泛的使用。高速蓝牙模块在2009年推出,速率提高到约24Mbps,传输速率是经典蓝牙的八倍,可以轻松的应用于录像机到电视、PC到PMP、UMPC到打印机之间的资料传输。

低功耗蓝牙BLE

是指支持蓝牙协议4.0或者以上的模块,也被称为BLE模块,最大的特点就是成本和功耗的降低,可以应用于实时性要求较高的产品当中,比如:智能家居类(蓝牙锁、蓝牙灯)、传感设备的数据发送(血压计、温度传感器)、消费类电子(电子烟、遥控玩具)等。

目前市面上大部分的蓝牙都是4.0以上的低功耗蓝牙,经典蓝牙已经很少见到了。

通讯过程

一个外围设备可以发布多个服务service,每个服务可以包含多个特征值characteristic,每个特征值都有他的属性,例如长度(size),权限(permission),值(value),描述(descriptor),读写通讯都是通过Characteristic进行的。
每个service、characteristic都含有一个对应的UUID,通过和外围设备蓝牙约定UUID来进行读写通讯。

整个通讯流程为:


图片.png

Android常用的第三方蓝牙框架:okbleAndroid-BluetoothKit

以okble为例:

package com.wrs.project.module.app.common.bluetooth;
import android.content.Context;
import android.util.Log;

import com.a1anwang.okble.client.core.OKBLEDevice;
import com.a1anwang.okble.client.core.OKBLEDeviceImp;
import com.a1anwang.okble.client.core.OKBLEDeviceListener;
import com.a1anwang.okble.client.core.OKBLEOperation;
import com.a1anwang.okble.client.scan.BLEScanResult;
import com.a1anwang.okble.client.scan.DeviceScanCallBack;
import com.a1anwang.okble.client.scan.OKBLEScanManager;
import com.a1anwang.okble.common.OKBLECharacteristicModel;
import com.a1anwang.okble.common.OKBLEServiceModel;
import com.wrs.project.module.app.common.AppMgr;

import java.util.List;

public class Bluetooth implements DeviceScanCallBack, OKBLEDeviceListener{
    private OKBLEScanManager scanManager;
    private OKBLEDevice okbleDevice;// 当前连接的蓝牙设备
    private OKBLECharacteristicModel writeCharacteristicModel; // 蓝牙可写的Characteristic
    private Context context = AppMgr.context;

    private String tag = "Bluetooth";

    public Bluetooth() {
        scanManager = new OKBLEScanManager(context);
        scanManager.setScanCallBack(this);
    }

    /**
     * 扫描到蓝牙设备
     * @param device
     * @param rssi
     */
    @Override
    public void onBLEDeviceScan(BLEScanResult device, int rssi) {
        Log.e(tag, "扫描到蓝牙设备:" + device.toString());
        String localName = device.getCompleteLocalName();
        if (null != localName && localName.startsWith("ABC-")) {
            stopScanBluetooth();
            connectBluetoothDevice(device);
        }
    }

    /**
     * 扫描失败
     * @param code
     */
    @Override
    public void onFailed(int code) {

    }

    @Override
    public void onStartSuccess() {

    }

    /**
     * 蓝牙是否已经开启
     * @return
     */
    public boolean bluetoothIsEnable() {
        if (null != scanManager) {
            scanManager.bluetoothIsEnable();
        }
        return false;
    }

    /**
     * 关闭手机蓝牙
     */
    public void disableBluetooth() {
        if (null != scanManager) {
            scanManager.disableBluetooth();
        }
    }

    /**
     * 打开手机蓝牙
     */
    public void enableBluetooth() {
        if (null != scanManager) {
            scanManager.enableBluetooth();
        }
    }

    /**
     * 开始扫描蓝牙
     */
    public void startScanBluetooth() {
        if (null != scanManager) {
            if (!scanManager.isScanning()) {
                scanManager.startScan();
            }
        }
    }

    /**
     * 停止扫描蓝牙
     */
    public void stopScanBluetooth() {
        if (null != scanManager) {
            if (scanManager.isScanning()) {
                scanManager.stopScan();
            }
        }
    }

    /**
     * 断开蓝牙连接
     */
    public void disConnect() {
        if (null != okbleDevice) { // 如果当前已经连接其他设备,先断开连接
            okbleDevice.removeDeviceListener(this);
            okbleDevice.disConnect(false);
            okbleDevice = null;
        }
    }

    /**
     * 发送数据给蓝牙设备
     * @param data
     */
    public void writeData(byte[] data) {
        if (null != okbleDevice && null != writeCharacteristicModel && null != data && data.length > 0) {
            okbleDevice.addWriteOperation(writeCharacteristicModel.getUuid(), data, new OKBLEOperation.WriteOperationListener() {
                @Override
                public void onWriteValue(byte[] value) {
                    Log.e(tag, "蓝牙写数据成功");
                }

                @Override
                public void onFail(int code, String errMsg) {
                    Log.e(tag, "蓝牙写数据失败:" + code + " " + errMsg);
                }

                @Override
                public void onExecuteSuccess(OKBLEOperation.OperationType type) {

                }
            });
        } else {
            Log.e(tag, "蓝牙写数据失败: 蓝牙没有连接或没发现可写Characteristic");
        }
    }

    public void connectBluetoothDevice(BLEScanResult device) {
        // 先断开当前连接
        disableBluetooth();
        okbleDevice = new OKBLEDeviceImp(context, device);
        okbleDevice.addDeviceListener(this);
        okbleDevice.connect(true);//true表示连接断开后OKBLE的会自动重连
    }

    /**
     * 蓝牙设备连接成功
     * @param deviceTAG
     */
    @Override
    public void onConnected(String deviceTAG) {
        Log.e(tag, "设备连接成功 " + deviceTAG);

        // 连上蓝牙后,获取蓝牙的WriteCharacteristic用后面给蓝牙设备发送数据,获取蓝牙的ReadCharacteristic用来监听蓝牙设备发送过来的数据
        List<OKBLEServiceModel> serviceModels = okbleDevice.getServiceModels();
        if (null != serviceModels && serviceModels.size() > 0) {
            for (int i = 0; i < serviceModels.size(); i++) {
                OKBLEServiceModel serviceModel = serviceModels.get(i);
                String serviceUUID = serviceModel.getUuid();
                if (serviceUUID.startsWith("aaaaaaa-")) { // 匹配找到读写的服务
                    List<OKBLECharacteristicModel> characteristicModels = serviceModel.getCharacteristicModels();
                    if (null != characteristicModels && characteristicModels.size() > 0) {
                        for (int j = 0; j < characteristicModels.size(); j++) {
                            OKBLECharacteristicModel characteristicModel = characteristicModels.get(j);
                            String characteristicUUID = characteristicModel.getUuid();
                            if (characteristicUUID.startsWith("bbbbbbbbb") && characteristicModel.isCanWrite() && characteristicModel.isCanWriteNoResponse()) { // 匹配找到写的Characteristic
                                findWriteCharacteristic(characteristicModel);
                            } else if (characteristicUUID.startsWith("8653000b-") && characteristicModel.isCanNotify()) { // 匹配找到读的Characteristic
                                findReadCharacteristic(characteristicModel);
                            }
                        }
                    }
                    break;
                }
            }
        }
    }

    @Override
    public void onDisconnected(String deviceTAG) {

    }

    @Override
    public void onReadBattery(String deviceTAG, int battery) {

    }

    /**
     * 接收到蓝牙发送的数据
     * @param deviceTAG
     * @param uuid
     * @param value
     */
    @Override
    public void onReceivedValue(String deviceTAG, String uuid, byte[] value) {

    }

    @Override
    public void onWriteValue(String deviceTAG, String uuid, byte[] value, boolean success) {

    }

    @Override
    public void onReadValue(String deviceTAG, String uuid, byte[] value, boolean success) {

    }

    @Override
    public void onNotifyOrIndicateComplete(String deviceTAG, String uuid, boolean enable, boolean success) {

    }

    private void findWriteCharacteristic(OKBLECharacteristicModel characteristic) {
        if (null != okbleDevice && null != characteristic) {
            writeCharacteristicModel = characteristic;
        }
    }

    private void findReadCharacteristic(OKBLECharacteristicModel characteristic) {
        if (null != okbleDevice && null != characteristic) {
            String uuid = characteristic.getUuid();
            boolean enableNotifyEnable = okbleDevice.isNotifyEnabled(uuid);
            if (enableNotifyEnable) {
                Log.e(tag, "打开读属性成功");
            } else {
                okbleDevice.addNotifyOrIndicateOperation(uuid, true, new OKBLEOperation.NotifyOrIndicateOperationListener() {

                    @Override
                    public void onFail(int code, String errMsg) {
                        Log.e(tag, "打开读属性失败");

                    }

                    @Override
                    public void onExecuteSuccess(OKBLEOperation.OperationType type) {
                        Log.e(tag, "打开读属性成功");

                    }

                    @Override
                    public void onNotifyOrIndicateComplete() {
                        Log.e(tag, "打开读属性成功");
                    }
                });

            }

        }
    }
}

蓝牙广播数据包解析

广播包有两种: 广播包 (Advertising Data)和 响应包 (Scan Response),其中广播包是每个设备必须广播的,而响应包是可选的。
每个包都是 31 字节,分为有效数据和无效数据两部分。

有效数据部分 :包含若干个广播数据单元,称为 AD Structure 。

AD Structure 的组成是:
第一个字节是长度值 Len ,表示接下来的 Len 个字节是数据部分。
数据部分的第一个字节表示数据的类型 AD Type ,剩下的 Len - 1 个字节是真正的数据 AD data 。其中 AD type 非常关键,决定了 AD Data 的数据代表的是什么和怎么解析。
无效数据部分 :因为广播包的长度必须是 31 个 byte,如果有效数据部 分不到 31 自己,剩下的就用 0 补全。这部分的数据是无效的,解释的时候,忽略即可。在 Android 中,系统会把这两个数据拼接在一起,返回一个 62 字节的数组。


图片.png

例如:


图片.png

第一个 字节代表广播数据单元的长度 ,02 转为10进制就是 2代表其数据长度为2 , 而数据单元的第一个字节代表类型 。
01 代表 代表物理连接功能为普通发现模式 06代表其数据类容
紧接着下一个数据单元:
0B代表数据长度为11 ,数据类型为 02 即Serviceuuid代表是非完整的16bit uuid, 所以紧接着的后10位就是其uuid。
接下来就是下一个数据单元
首位是13转为二进制就是19,其长度就是19,类型就是09 ,代表设备名称,30-》字符0,65代表字符e,61代表字符a,73代表字符s,79代表字符y,4E代表N,65代表e,57代表W,44代表D,43代表C,53代表S ,00 代表字符null,01代表字符soh(SOH是序始字符(Start Of Header),它表示标题的开始),56代表V ,31代表字符1,2E代表字符.,30代表字符0,44代表D所有其设备名称就是0easyNewDCS V1.0D。
接下来的一个数据单元长度是5,广播类型12 连接间隔范围,有四个字节,接下来数据长度是02,类型是0A代表信号强度 剩余都是00000都是补位的无效数据。

广播数据类型:
(1)Flags: TYPE = 0x01。这个数据用来标识设备 LE 物理连接的功能。DATA 是 0 到多个字节的 Flag 值,每个 bit 上用 0 或者 1 来表示是否为 True。如果有任何一个 bit 不为 0,并且广播包是可连接的,就必须包含此数据。各 bit 的定义如下: bit 0: LE 有限发现模式 bit 1: LE 普通发现模式 bit 2: 不支持 BR/EDR bit 3: 对 Same Device Capable(Controller) 同时支持 BLE 和 BR/EDR bit 4: 对 Same Device Capable(Host) 同时支持 BLE 和 BR/EDR bit 5..7: 预留
(2)Service UUID: 广播数据中一般都会把设备支持的 GATT Service 广播出来,用来告诉外面本设备所支持的 Service。有三种类型的 UUID:16 bit, 32bit, 128 bit。广播中,每种类型类型有有两个类别:完整和非完整的。这样就共有 6 种 AD Type。
非完整的 16 bit UUID 列表: TYPE = 0x02;
完整的 16 bit UUID 列表: TYPE = 0x03;
非完整的 32 bit UUID 列表: TYPE = 0x04;
完整的 32 bit UUID 列表: TYPE = 0x05;
非完整的 128 bit UUID 列表: TYPE = 0x06;
完整的 128 bit UUID 列表: TYPE = 0x07;
(3) Local Name: 设备名字,DATA 是名字的字符串。 Local Name 可以是设备的全名,也可以是设备名字的缩写,其中缩写必须是全名的前面的若干字符。 设备全名: TYPE = 0x08 设备简称: TYPE = 0x09
(4)TX Power Level: TYPE = 0x0A,表示设备发送广播包的信号强度。DATA 部分是一个字节,表示 -127 到 + 127 dBm。
(5) 带外安全管理(Security Manager Out of Band):TYPE = 0x11。DATA 也是 Flag,每个 bit 表示一个功能: bit 0: OOB Flag,0 表示没有 OOB 数据,1 表示有 bit 1: 支持 LE bit 2: 对 Same Device Capable(Host) 同时支持 BLE 和 BR/EDR bit 3: 地址类型,0 表示公开地址,1 表示随机地址 。
(6)外设(Slave)连接间隔范围:TYPE = 0x12。数据中定义了 Slave 最大和最小连接间隔,数据包含 4 个字节:
前 2 字节:定义最小连接间隔,取值范围:0x0006 ~ 0x0C80,而 0xFFFF 表示未定义; 后 2 字节:定义最大连接间隔,同上,不过需要保证最大连接间隔大于或者等于最小连接间隔。
(7) 服务搜寻:外围设备可以要请中心设备提供相应的 Service。其数据定义和前面的 Service UUID 类似:
16 bit UUID 列表: TYPE = 0x14
32 bit UUID 列表: TYPE = 0x??
128 bit UUID 列表: TYPE = 0x15
(8) Service Data: Service 对应的数据。
16 bit UUID Service: TYPE = 0x16, 前 2 字节是 UUID,后面是 Service 的数据;
32 bit UUID Service: TYPE = 0x??, 前 4 字节是 UUID,后面是 Service 的数据;
128 bit UUID Service: TYPE = 0x??, 前 16 字节是 UUID,后面是 Service 的数据;
(9) 公开目标地址:TYPE = 0x17,表示希望这个广播包被指定的目标设备处理,此设备绑定了公开地址,DATA 是目标地址列表,每个地址 6 字节。
(10) 随机目标地址:TYPE = 0x18,定义和前一个类似,表示希望这个广播包被指定的目标设备处理,此设备绑定了随机地址,DATA 是目标地址列表,每个地址 6 字节。
(11) Appearance:TYPE = 0x19,DATA 是表示了设备的外观。
(12) 厂商自定义数据: TYPE = 0xFF,厂商自定义的数据中,前两个字节表示厂商 ID,剩下的是厂商自己按照需求添加,里面的数据内容自己定义。

源码下载

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

推荐阅读更多精彩内容