蓝牙Beacon广播数据包格式以及解析

内容转载自我的博客
如果你只想找到如何用代码解析各数据请点击目录"使用Java解析数据"

1. 获取原始蓝牙广播包

首先需要开启开发者选项:不同Android手机打开此功能的方法基本一致,首先打开设置,然后找到系统版本号(例如MIUI系统的全部参数选项的MIUI版本),快速连续点击5次以上即可自动打开开发者选项;然后选择"打开蓝牙数据包日志"功能,接着打开蓝牙功能即可开始记录数据包,日志文件存放位置在不同的手机上略有不同;最后把日志复制到电脑上等待处理

2. 安装WireShark软件

对于ubuntu系统来说,只需要输入以下命令即可成功安装:
sudo apt-get install wireshark
对于windows或其他系统来说,打开官网按照提示下载安装即可

3. 分析Beacon广播包数据

把日志文件导入WireShark软件,会自动识别为蓝牙广播包。首先需要了解蓝牙数据包的主要格式:一个广播包是由若干个广播单元AD Structure构成的。每个广播单元的组成是:第一个字节是长度值 length,表示接下来的 length个字节是数据部分;数据部分的第一个字节表示数据的类型AD Type,AD type非常关键,决定了AD Data的数据代表的是什么以及怎么解析,这是官网上面不同的值代表的数据类型 https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile ;剩下的length-1个字节是真正的数据AD data。这里需要特别注意的是,由于发送数据是从低位到高位依次发送,所以接收到的数据要反过来按字节拼接。例如接收到的MAC为 8b 03 00 b0 01 c2,那么实际的MAC为 c2:01:b0:00:03:8b
通过分析可以发现,蓝牙设备会连续收到两个来自(同一个)Beacon的广播数据包,每个原始数据包都是59bytes,前一个主要包含MAC和设备名称等信息,后一个主要包含UUID,txPower等信息。不妨认为前一个数据包为packetA,后一个为packetB。以下是一组实际数据

3.1 第一个数据包格式

使用WireShark捕获的第一个数据包

可以看到,软件会自动把每个部分的数据进行解释,如果你的英语水平可以的话,以下内容就不需要看了。数据包内容:

04 3e 38 0d 01 1b 00 01 8b 03 00 b0 01 c2 01 00 ff 7f af 00 00 00 00 00 00 00 00 00 1e
02 0a 00 08 16 f0 ff 64 27 11 4c b9 11 09 4d 69 6e 69 42 65 61 63 6f 6e 5f 30 30 39 30 37

下面是每个字节对应的含义:

第一个字节是HCI Packet Type,04表示这是HCI Event;剩下的58bytes则是HCI Event的具体内容
第二个字节是EventCode,3e是此事件的代码;第三个字节是Parameter Length,0x38(十进制56)表示后面数据长度56bytes
第四个字节是SubEvent,0d表示这是LE Extended Advertising Report;第五个字节是Num Reports,数值为01
1b 00这两个字节代表Event Type,由于发送数据都是按字节发送以及从低位向高位发送,因此真实值是 001b
01 表示这是随机设备地址
8b 03 00 b0 01 c2 是此设备的MAC,根据从低向高的发送规则,所以真实MAC是 c2:01:b0:00:03:8b
01 代表首要广播信道的带宽
00 代表次要广播信道的带宽,此处表示不使用次要信道
ff 表示广播SID
7f 代表Tx Power的大小,此处是127dbm
af 代表RSSI的大小,此处是-81dbm
00 00 代表周期广播间隔
00 代表直接地址类型,次数是公共设备地址
00 00 00 00 00 00 代表直接BD_ADDR
1e 代表接下的的数据的字节数(长度),以下数据就是最重要的广播数据了
-------------------------------------------
02 0a 00 代表的是Tx Power Level的信息,02 表示数据字节数,0a 表示数据类型,00 表示功率水平(单位是dBm)
08 16 f0 ff 代表的是Service Data信息,08 表示数据字节数,16 表示数据类型,ff f0 表示16bit UUID ,
64 27 11 4c b9 表示Service Data的具体信息
11 表示后面的数据的字节数
09 表示数据类型
4d 69 6e 69 42 65 61 63 6f 6e 5f 30 30 39 30 37 表示设备的名称,每一个字节对应一个ASCII

如果你想查看WireShark软件的解析名称,可在附录里浏览

3.2 第二个数据包格式

使用WireShark捕获的第一个数据包

此数据包才是最重要的,59bytes的数据包内容如下:

04 3e 38 0d 01 13 00 01 8b 03 00 b0 01 c2 01 00 ff 7f af 00 00 00 00 00 00 00 00 00 1e
02 01 06 1a ff 4c 00 02 15 fd a5 06 93 a4 e2 4f b1 af cf c6 eb 07 64 78 25 27 11 4c b9 c5

下面是每个字节对应的含义:

第一行数据同上,不再分析,重点分析第二行(也就是广播数据部分)
--------------------------------------
02 表示接下来的数据有两个字节
01 表示数据类型,此处的类型是Flags
06 表示Flags的具体模式
1a 表示接下来的数据有26个字节
ff 表示数据类型,此处是厂家特定字(Manufacturer specific)
4c 00 表示公司的ID,此处的004c代表苹果公司
02 代表beacon标识位
15 表示接下来有22个字节的数据
fd a5 06 93 a4 e2 4f b1 af cf c6 eb 07 64 78 25 表示beacon UUID
27 11 是major的值,2711转化为10进制是10001
4c b9 是minor的值,4cb9转化为10进制是19641
c5 是txPower的补码,计算可知原码是-59

如果你想查看WireShark软件的解析名称,可在附录里浏览

3.3 Android程序开发中的蓝牙广播包

上面所讲解的都是Android 系统中的蓝牙广播包的格式,是最底层的数据包格式,如果我们是在开发OS 的话才可能会接触解析这些数据。对于只是进行普通应用程序开发的我们来说,只需要处理已经被Android 系统一次解析之后的数据包,这个数据包才是我们开发应用程序时遇到的数据记录。  
对于Android 开发中,系统会把packetB中的第二行数据(30bytes,长度不定)和packetA(30bytes)中的第二行数据连接在一起,最后总的数据长度为62bytes,不够的话用0填充,如下所示:
02 01 06 1a ff 4c 00 02 15 fd a5 06 93 a4 e2 4f b1 af cf c6 eb 07 64 78 25 27 11 4c b9 c5 02 0a 00 08 16 f0 ff 64 27 11 4c b9 11 09 4d 69 6e 69 42 65 61 63 6f 6e 5f 30 30 39 30 37 00 00

4. 使用Java解析各数据

以下是主要解析代码,兼容Android P最新版本和Android 较低版本:

//Android Lollipop 版本以上的扫描回调函数
private ScanCallback mScanCallback = new ScanCallback() {
    @Override
    public void onScanResult(int callbackType, ScanResult result) {
        super.onScanResult(callbackType, result);
        final BluetoothDevice device = result.getDevice();
        final int rssi = result.getRssi();
        final byte[] scanRecord = result.getScanRecord().getBytes();
        // 判断Activity是否已经退出
        if (mainActivity == null) {
            return;
        }
        mainActivity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                String name = device.getName();
                if (name == null) {
                    //Log.d(TAG, "onLeScan: 此条数据被过滤---"+mac);
                    return;
                }
                Log.d(TAG, "onScanResult: 开始处理广播数据");
                handleScanResult(device, rssi, scanRecord);
            }
        });
    }

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

    @Override
    public void onScanFailed(int errorCode) {
        super.onScanFailed(errorCode);
        Log.e(TAG, "启动扫描失败");
    }
};

// Android KITKAT 版本以下的扫描回调函数
private BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
    @Override
    public void onLeScan(final BluetoothDevice device, final int rssi, final byte[] scanRecord) {
        // 判断Activity是否已经退出
        if (mainActivity == null) {
            return;
        }
        mainActivity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                String name = device.getName();
                String mac = device.getAddress();
                // 官方提供的过滤UUID方法存在问题,此处自己先根据MAC过滤
                if (name == null || mac == null || !Arrays.asList(beaconMAC).contains(mac)) {
                    //Log.d(TAG, "onLeScan: 此条数据被过滤---"+mac);
                    return;
                }
                Log.d(TAG, "onLeScan: 开始处理广播数据");
                handleScanResult(device, rssi, scanRecord);
            }
        });
    }
};

// 处理广播数据
private void handleScanResult(BluetoothDevice device, int rssi, byte[] scanRecord) {
    int startIndex = 2;
    boolean patternFound = false;
    Log.d(TAG, "handleScanResult: "+bytesToHex(scanRecord));
    // 寻找是否存在beacon以及有效数据的起始索引
    while (startIndex <= 5) {
        if (((int) scanRecord[startIndex + 2] & 0xff) == 0x02
                && ((int) scanRecord[startIndex + 3] & 0xff) == 0x15) {
            patternFound = true;
            break;
        }
        startIndex++;
    }
    // 如果找到
    if (patternFound) {
        String ibeaconName = device.getName();
        String mac = device.getAddress();
        Log.d(TAG, "onLeScan: 搜索到一个Beacon设备" + mac);
        Log.d(TAG, "onLeScan: 它的名字是" + ibeaconName);
        String data = parseBLEData(scanRecord, startIndex);
        Log.d(TAG, "onLeScan: RSSI=" + rssi);
        String result = "Beacon设备名称:" + ibeaconName + "  MAC:" + mac + "\n";
        double distance = rssi2distance(rssi);
        String dis = String.format("%.2f", distance);
        result = result + data + "    RSSI:" + rssi + " ( " + dis + " )";
        Log.d(TAG, result);
    } else {
        String name = device.getName();
        if (name == null) {
            //Log.d(TAG, "onLeScan: 搜索到一个普通蓝牙设备,它的MAC是"+device.getAddress());
        } else {
            //Log.d(TAG, "onLeScan: 搜索到一个普通蓝牙设备,它的名字是"+name);
        }
    }
}

// 根据RSSI计算距离
public double rssi2distance(int rssi) {
    int iRssi = Math.abs(rssi);
    // 发射端和接收端相隔1米时的信号强度
    int A = 59;
    // 环境噪声衰减因子
    double n = 2.0;
    double power = (iRssi - A) / (10 * n);
    return Math.pow(10, power);
}

// 字节数据转为Hex: 1 byte = 8bit = 两个16进制数字
public String bytesToHex(byte[] src) {
    StringBuilder stringBuilder = new StringBuilder("");
    for (int i = 0; i < src.length; i++) {
        int v = src[i] & 0xFF;
        String hv = Integer.toHexString(v);
        if (hv.length() < 2) {
            stringBuilder.append(0);
        }
        stringBuilder.append(hv);
    }
    return stringBuilder.toString();
}

// 格式化UUID
public String parseUUID(String data) {
    String uuid = "";
    if (data.length() == 32) {
        uuid = data.substring(0, 8) + "-"
                + data.substring(8, 12) + "-"
                + data.substring(12, 16) + "-"
                + data.substring(16, 20) + "-"
                + data.substring(20);
    } else {
        showTips(getString(R.string.toast_uuid_not_found));
    }
    return uuid;
}

// 解析BLE数据
public String parseBLEData(byte[] scanRecord, int startIndex) {
    // uuid的长度是16bytes
    byte[] uuidBytes = new byte[16];
    System.arraycopy(scanRecord, startIndex + 4, uuidBytes, 0, 16);
    String hexString = bytesToHex(uuidBytes);
    // beacon的UUID值
    String uuid = parseUUID(hexString);
    // beacon的Major值
    int major = (scanRecord[startIndex + 20] & 0xff) * 0x100
            + (scanRecord[startIndex + 21] & 0xff);
    // ibeacon的Minor值
    int minor = (scanRecord[startIndex + 22] & 0xff) * 0x100
            + (scanRecord[startIndex + 23] & 0xff);

    int txPower = (scanRecord[startIndex + 24]);
    Log.d(TAG, "onLeScan: 它的UUID是" + uuid + ",txPower是" + txPower);
    Log.d(TAG, "onLeScan: major=" + major + ",minor=" + minor);
    return "UUID:" + uuid + "\nmajor:" + major + "    minor:" + minor + "\ntxPower:" + txPower;
}

以上就是主要代码

5. 附录

5.1 第一个数据包的内容以及解析

数据包内容:

04 3e 38 0d 01 1b 00 01 8b 03 00 b0 01 c2 01 00 ff 7f af 00 00 00 00 00 00 00 00 00 1e
02 0a 00 08 16 f0 ff 64 27 11 4c b9 11 09 4d 69 6e 69 42 65 61 63 6f 6e 5f 30 30 39 30 37

下面是每个字节对应的含义(WireShark软件的解析):

Bluetooth HCI H4
    [Direction: Rcvd (0x01)]
    HCI Packet Type: HCI Event (0x04)
Bluetooth HCI Event - LE Meta
    Event Code: LE Meta (0x3e)
    Parameter Total Length: 56
    Sub Event: LE Extended Advertising Report (0x0d)
    Num Reports: 1
    Event Type: 0x001b, Connectable, Scannable, Scan Response, Legacy, Data Status: Complete
        .... .... .... ...1 = Connectable: True
        .... .... .... ..1. = Scannable: True
        .... .... .... .0.. = Directed: False
        .... .... .... 1... = Scan Response: True
        .... .... ...1 .... = Legacy: True
        .... .... .00. .... = Data Status: Complete (0x0)
        0000 0000 0... .... = Reserved: 0x000
    Peer Address Type: Random Device Address (0x01)
    BD_ADDR: c2:01:b0:00:03:8b (c2:01:b0:00:03:8b)
    Primary PHY: LE 1M (0x01)
    Secondary PHY: No packets on the secondary advertising channel (0x00)
    Advertising SID: 0xff (not available)
    TX Power: 127dBm (not available)
    RSSI: -81dBm
    Periodic Advertising Interval: 0x0000 (no periodic advertising)
    Direct Address Type: Public Device Address (0x00)
    Direct BD_ADDR: 00:00:00_00:00:00 (00:00:00:00:00:00)
    Data Length: 30
    Advertising Data
        Tx Power Level
            Length: 2
            Type: Tx Power Level (0x0a)
            Power Level (dBm): 0
        Service Data - 16 bit UUID
            Length: 8
            Type: Service Data - 16 bit UUID (0x16)
            UUID 16: Unknown (0xfff0)
            Service Data: 6427114cb9
        Device Name: MiniBeacon_00907
            Length: 17
            Type: Device Name (0x09)
            Device Name: MiniBeacon_00907

5.2 第二个数据包的内容以及解析

数据包内容:

04 3e 38 0d 01 13 00 01 8b 03 00 b0 01 c2 01 00 ff 7f af 00 00 00 00 00 00 00 00 00 1e
02 01 06 1a ff 4c 00 02 15 fd a5 06 93 a4 e2 4f b1 af cf c6 eb 07 64 78 25 27 11 4c b9 c5

下面是每个字节对应的含义(WireShark软件的解析):

Bluetooth HCI H4
    [Direction: Rcvd (0x01)]
    HCI Packet Type: HCI Event (0x04)
Bluetooth HCI Event - LE Meta
    Event Code: LE Meta (0x3e)
    Parameter Total Length: 56
    Sub Event: LE Extended Advertising Report (0x0d)
    Num Reports: 1
    Event Type: 0x0013, Connectable, Scannable, Legacy, Data Status: Complete
        .... .... .... ...1 = Connectable: True
        .... .... .... ..1. = Scannable: True
        .... .... .... .0.. = Directed: False
        .... .... .... 0... = Scan Response: False
        .... .... ...1 .... = Legacy: True
        .... .... .00. .... = Data Status: Complete (0x0)
        0000 0000 0... .... = Reserved: 0x000
    Peer Address Type: Random Device Address (0x01)
    BD_ADDR: c2:01:b0:00:03:8b (c2:01:b0:00:03:8b)
    Primary PHY: LE 1M (0x01)
    Secondary PHY: No packets on the secondary advertising channel (0x00)
    Advertising SID: 0xff (not available)
    TX Power: 127dBm (not available)
    RSSI: -81dBm
    Periodic Advertising Interval: 0x0000 (no periodic advertising)
    Direct Address Type: Public Device Address (0x00)
    Direct BD_ADDR: 00:00:00_00:00:00 (00:00:00:00:00:00)
    Data Length: 30
    Advertising Data
        Flags
            Length: 2
            Type: Flags (0x01)
            000. .... = Reserved: 0x0
            ...0 .... = Simultaneous LE and BR/EDR to Same Device Capable (Host): false (0x0)
            .... 0... = Simultaneous LE and BR/EDR to Same Device Capable (Controller): false (0x0)
            .... .1.. = BR/EDR Not Supported: true (0x1)
            .... ..1. = LE General Discoverable Mode: true (0x1)
            .... ...0 = LE Limited Discoverable Mode: false (0x0)
        Manufacturer Specific
            Length: 26
            Type: Manufacturer Specific (0xff)
            Company ID: Apple, Inc. (0x004c)
            Data: 0215fda50693a4e24fb1afcfc6eb0764782527114cb9c5
                [Expert Info (Note/Undecoded): Undecoded]
                    [Undecoded]
                    [Severity level: Note]
                    [Group: Undecoded]
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,710评论 4 376
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,839评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,295评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,776评论 0 223
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,198评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,074评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,200评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,986评论 0 214
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,733评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,877评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,348评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,675评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,393评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,209评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,212评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,003评论 2 280

推荐阅读更多精彩内容