Beacon低功耗蓝牙基本使用

2,255 阅读13分钟

一、概述

1.1 Beacon简介

 iBeacon是基于Android的低功耗蓝牙BLE,和蓝牙不同的是iBeacon不用配对和连接过程,iBeacon基站通过BLE蓝牙的广播频道不断向外发送位置信息,当智能设备进入设定区域时,只要满足iBeacon技术标准,不需要连接就能够收到信号。

事实上,IBeacon最早的提出是苹果公司,它们在2013年9月发布的移动设备上就已经使用了IBeacon技术,随后,许多组织开始着力开发蓝牙 Beacon 试点项目,到 2015 年,已经有许多公司开始尝试利用部署好的蓝牙 beacon 探索新的服务模式。IBeacon可以通过rssi来判断设备与基站的距离,用于室内的定位、微信签到等场景。 

Beacon 具有如下一些特点:

  • Beacon 是使用蓝牙4.0(BLE)技术发射信号的小设备
  • 有效范围从几十厘米到几米,电池可用3年
  • 信号为单向发射,只能发送小数据量,例如一个 128 bit的ID
  • 智能手机通常作为接收方

总的来说,Beacon 就是一个室内蓝牙导航的“GPS卫星”,可以向覆盖区域的用户,自动发送信息,并判断用户所处位置,再基于位置传达相应信息。Beacon 传播速度快,安全性高,传输距离远,应用领域和市场前景广阔,引起诸多高新企业在此技术上发力。

1.2 Beacon 标准

蓝牙无线通讯技术作为一种无线数据与语音通信的开放性全球标准,最开始的应用是在语音通信领域取代耳机线。直至 4.0 版本推出的低功耗蓝牙技术在智能可穿戴设备与智能家居设备中应用及其广泛,这些都是从最初蓝牙耳机时代逐渐革新升级过来的,现在蓝牙技术应用的智能设备几乎成为白领们追赶潮流的标志。

Beacon 的标准包括信号数据的格式等,苹果和谷歌各有一套标准,苹果标准更早,谷歌的标准更加强大。

  • Apple iBeacon 2013 6 月发布
  • Google Eddystone 2015,7 月发布

1.3 Beacon 技术要点

1.3.1 iBeacon组成

iBeacon的核心内容如下:

  • UUID:厂商识别号
  • Major:相当于群组号,同一个组里Beacon有相同的Major
  • Minor:相当于识别群组里单个的Beacon
  • TX Power:用于测量设备离Beacon的距离,目前只定义了大概的3个粗略级别10里面内,一米内,一米外

 UUID+Major+Minor就构成了一个Beacon的识别号,有点类似于网络中的IP地址。可以根据TX Power和rssi等信息通过算法计算出定位信息。    iBeacon中一般有两个角色:

  • 基站/从机/外围设备(peripheral),发射端
  • 手机/主机/中心设备(central),接收者

1.3.2 Beacon广播

Beacon 会每隔一定的时间广播一个数据包到周围,作为独立的蓝牙主机(比如手机等)在执行扫描动作时,会间隔地接收到 Beacon 广播出来的数据包。该数据包内容最多可以包含 31 个字节的内容。同时,在主机接收到广播包时,其中会指示该广播包来自于哪一个蓝牙从机 MAC 地址(每个 Beacon 拥有唯一的 MAC 地址)的从机设备和当前的接收发送信号强度指示值 RSSI 为多少。这时候,如果手机上安装有 Beacon 对应的 APP,接收到该 I D的 APP 会根据该 ID 的设置条件采取相应的动作!作为 Beacon 设备放在室内的某个固定位置,被设置成广播模式,上电后即进行广播,不能和任何低功耗蓝牙主机进行连接。

BLE 协议栈采用了分层结构,其中有一层称为 GAP(Generic Access Profile),该层负责设备间的广播,搜寻以及连接。搜寻过程涉及广播设备和扫描设备。广播设备以固定的间隔向外广播数据包,这些数据包中的信息可以帮助扫描设备确定该设备是否是其感兴趣的设备。

Beacon 设备也进行广播,但是其它设备不需要与其进行连接。我们可以称其为广播者(BLE 规范中广播者只广播不接受连接请求)。手机应用只需要利用广播包中的相关信息就可以实现定位的目的,因此不需要与 Beacon 设备进行连接。

1.3.3 Beacon 帧格式

广播数据包最多仅可以包含 31 字节数据,所以设计者必须慎重选择需要包含的数据。蓝牙 SIG 组织在 Core Specification Supplement (CSS)文件中将这 31 个字节数据分成多个 AD Type 结构,每个 AD Type 都有相同的结构,分别为长度字节,类型字节以及数据域。

Beacon 设备可以在一个或多个标准广播数据包中编码数据,传递信息。但是编码原理可能有所差异,即帧格式不同。目前主流的三种帧格式分别为苹果公司的 iBeacon,Radius Networks 公司的AltBeacon 以及谷歌公司的 Eddystone。因此为了与不同的beacon设备进行交互,应用开发者在开发beacon应用时需要了解对应设备的帧格式。

1.4 Beacon应用场景

Beacon 是一种通过低功率蓝牙技术(BluetoothLowEnergy)实现精确定位的设备。当室内某个位置安装了一个 Beacon 信号发射基站时,这个基站会创建一个信号区域。用户携带着移动设备进入信号区域的时候,相应的程序便会主动提示用户是否需要接入这个信号网络,同时手机中的应用程序会把用户离信号发射基站的距离划分为近(near),适中(medium)和远(far),并由此精确的对用户进行定位,依据用户处于的特定情境向他传达相应的信息。

时下,蓝牙 Beacon 在国外超市、机场的部署已经相当普遍,尤其用在蓝牙室内定位方面,Beacon 的使用是蓝牙应用的方向和趋势之一,下面是一些常用的场景分析。

  • 信息推送(商场、景区、博物馆)
  • 蓝牙beacon室内导航(会展、图书馆、博物馆、酒店、机场、校园、医院、监狱、商业CBD)
  • beacon和微信摇一摇结合,可以开展新的营销和互动方式,比如摇关注、摇签到、摇投票、摇导航、摇互动
  • 汽车(蓝牙钥匙) 

在大多数应用场景中,蓝牙beacon主要用于室内定位。不论在商场,机场,办公室,还是博物馆,都可以利用这些位置信息来提供服务。在这些案例中,通过将蓝牙 Beacon 安装在特定位置,然后利用智能手机上的 APP 与之交互,就可以达到定位的效果。

1.5 Beacon 和蓝牙的区别

iBeacon 它是一种低耗能蓝牙技术。是苹果公司2013年9月发布的移动设备用 OS(iOS7)上配备的新功能。其工作方式是,配备有低功耗蓝牙(BLE)通信功能的设备使用 BLE 技术向周围发送自己特有的 ID,接收到该 ID 的应用软件会根据该 ID 采取一些行动。

蓝牙技术是一种无线技术标准,可实现固定设备、移动设备和楼宇个人域网之间的短距离数据交换(使用2.4—2.485GHz的ISM波段的UHF无线电波)。

在很多硬件人员的眼中认为,iBeacon 和 BLE 没有区别啊,都是在同一个模块上面开发的,只是发送的数据格式不一样,iBeacon 应该和 BLE 没有区别,这个其实是一个错误,在 iOS 的开发过程中 iBeacon 和 BLE 是两个不同的东西,所有的数据都被苹果拦截了,只给开发者特定的 API 可以调用。

二、Android Beacon集成

2.1 集成Beacon

Github链接:android-beacon-library

首先,我们在Android项目中添加android-beacon-library库的依赖,如下所示。

dependencies {
    ...
    implementation 'org.altbeacon:android-beacon-library:${altbeacon.version}'
    ...
}

Beacon的使用主要分为广播信号发射端和信号的接收端,下面我们就从这两个端来拆分学习。

2.2 发射广播信号的IBeacon

参考文档:Android Beacon Library Document

广播参数

在正式发射广播之前,需要设置一些默认的参数,比如:

    public AdvertiseSettings createAdvertiseSettings(boolean isConnect, int timeoutMillis) {
        return new AdvertiseSettings.Builder()
                .setConnectable(isConnect)
                .setTimeout(timeoutMillis)
                .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
                .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
                .build();
    }

下面分别说明一下这几个函数的作用:

setTxPowerLevel

  • ADVERTISE_TX_POWER_LOW:低功率
  • ADVERTISE_TX_POWER_MEDIUM:中等功率
  • ADVERTISE_TX_POWER_HIGH:高功率,功率越高越耗性能

setAdvertiseMode

  • ADVERTISE_MODE_LOW_LATENCY :在低延迟、高功率模式下执行蓝牙 LE 广播
  • ADVERTISE_MODE_LOW_POWER :在低功耗模式下执行蓝牙 LE 广播
  • ADVERTISE_MODE_BALANCED :在平衡功率模式下执行蓝牙 LE 广播

  setTimeout

设置超时时间,默认0表示用不超时,最大超时时间180秒,如果设置时间超过180秒则会抛出异常。

  setConnectable

是否开启运行设备自动连接功能

设置广播数据

设置广播数据使用的是AdvertiseData进行包装,如下:

    private AdvertiseData createAdvertiseData(String beaconUuid) {
        AdvertiseData.Builder builder = new AdvertiseData.Builder()
                //广播服务的UUID
                //.addServiceUuid(parcelUuid)
                //添加服务数据UUID和服务数据
                //.addServiceData(parcelUuid, "ee1".getBytes())
                //添加制造商ID和数据
                .addManufacturerData(176, mManufacturerData.array());
        //广播是否包含设备名
        // .setIncludeDeviceName(true)
        //广播是否包含发射功率等级
        //.setIncludeTxPowerLevel(true);
        return builder.build();
    }
  • addServiceUuid

设置广播服务的UUID,示例代码如下:

private static final String BEACON_UUID = "0000180d-0000-1000-8000-00805f9b34fb";
ParcelUuid parcelUuid = ParcelUuid.fromString(BEACON_UUID);
.addServiceUuid(parcelUuid)

  • addServiceData

用于添加服务数据UUID和服务数据,调用此参数后,就可以在startScan的onScanResult中通过result.getScanRecord().getServiceData(parcelUuid)获取到,否则为null。示例如下

.addServiceData(parcelUuid, "e1".getBytes())
  • addManufacturerData

添加制造商ID和数据,设置了该参数,就可以在startScan的onScanResult中通过result.getScanRecord().getManufacturerSpecificData(id)获取到,否则为null。接收两个参数,参数1是manufacturerId表示设置的id,可以任意定义,参数2表示厂家设备信息的名称的二进制数据,有严格的格式要求。格式如下:

.addManufacturerData(176, mManufacturerData.array());

制造商ID和数据填充

制造商ID和数据需要进行严格的设置,否则在Android的接收端通过BLE蓝牙接收到的信息无法解析UUID、Major、Minor、TxPower等信息,导致解析出错。  iBeacon 的 ManufacturerData(厂商自定义信息)格式如下:

  • 01 byte    type = 0x02 指明它是 iBeacon 帧
  • 01 byte    len = 0x15 = 21
  • 16 byte    UUID
  • 02 byte    major
  • 02 byte    minor
  • 01 byte    tx power

 

使用示例如下:

ByteBuffer mManufacturerData = ByteBuffer.allocate(23);
byte[] uuidByte = getIdAsByte(uuid);
mManufacturerData.put(0, (byte) 0x02);
mManufacturerData.put(1, (byte) 0x15);
for (int i = 2; i <= 17; i++) {
    mManufacturerData.put(i, uuidByte[i - 2]); // adding the UUID
}
mManufacturerData.put(18, (byte) 0x00); // first byte of Major
mManufacturerData.put(19, (byte) 0x09); // second byte of Major
mManufacturerData.put(20, (byte) 0x00); // first minor
mManufacturerData.put(21, (byte) 0x06); // second minor
mManufacturerData.put(22, (byte) 0xB5); // txPower

 

    public byte[] getIdAsByte(UUID uuid) {
        ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
        bb.putLong(uuid.getMostSignificantBits());
        bb.putLong(uuid.getLeastSignificantBits());
        return bb.array();
    }

开始广播iBeacon数据

只需要调用startAdvertising方法即可广播iBeacon数据,示例如下:

    public void startAdvertise(Context context) {
        BluetoothManager manager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
        BluetoothAdapter adapter = manager.getAdapter();
        advertiser = adapter.getBluetoothLeAdvertiser();
        advertiser.startAdvertising(
                createAdvertiseSettings(true, 0), //广播参数,默认0表示用不超时,最大超时时间180秒
                createAdvertiseData(BEACON_UUID), //广播数据
                advCallback);//内容回调
    }

 

 private AdvertiseCallback advCallback = new AdvertiseCallback() {
 
        @Override
        public void onStartSuccess(android.bluetooth.le.AdvertiseSettings settingsInEffect) {
            if (settingsInEffect != null) {
                String log = "onStartSuccess TxPowerLv="
                        + settingsInEffect.getTxPowerLevel()
                        + " mode=" + settingsInEffect.getMode()
                        + " timeout=" + settingsInEffect.getTimeout();
                LogUtils.d(log);
                Toast.makeText(context, log, Toast.LENGTH_LONG).show();
            } else {
                LogUtils.d("onStartSuccess, settingInEffect is null");
                Toast.makeText(context, "onStartSuccess, settingInEffect is null", Toast.LENGTH_LONG).show();
            }
        }
 
        @Override
        public void onStartFailure(int errorCode) {
            LogUtils.d("onStartFailure errorCode:" + errorCode);//返回的错误码
            if (errorCode == ADVERTISE_FAILED_DATA_TOO_LARGE) {
                Toast.makeText(context, "广播开启错误,数据大于31个字节", Toast.LENGTH_LONG).show();
                LogUtils.d("广播开启错误,数据大于31个字节");
            } else if (errorCode == ADVERTISE_FAILED_TOO_MANY_ADVERTISERS) {
                Toast.makeText(context, "未能开始广播,没有广播实例", Toast.LENGTH_LONG).show();
                LogUtils.d("未能开始广播,没有广播实例");
            } else if (errorCode == ADVERTISE_FAILED_ALREADY_STARTED) {
                Toast.makeText(context, "正在连接的,无法再次连接", Toast.LENGTH_LONG).show();
                LogUtils.d("正在连接的,无法再次连接");
            } else if (errorCode == ADVERTISE_FAILED_INTERNAL_ERROR) {
                Toast.makeText(context, "由于内部错误操作失败", Toast.LENGTH_LONG).show();
                LogUtils.d("由于内部错误操作失败");
            } else if (errorCode == ADVERTISE_FAILED_FEATURE_UNSUPPORTED) {
                Toast.makeText(context, "在这个平台上不支持此功能", Toast.LENGTH_LONG).show();
                LogUtils.d("在这个平台上不支持此功能");
            } else {
                Toast.makeText(context, "onStartFailure errorCode:" + errorCode, Toast.LENGTH_LONG).show();
            }
        }
    };

 

停止广播iBeacon数据

停止广播只需要调用stopAdvertising方法即可。

    private void stopAdvertise() {
        if (advertiser != null) {
            advertiser.stopAdvertising(advCallback);
            advertiser = null;
        }
    }

 

2.3 接收iBeacon广播

申请权限

和BLE一样,IBeacon的接收也需要系统权限,必须的权限如下:

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

如果使用动态方式来申请权限,也可以使用下面的代码:

String[] p = new String[]{Manifest.permission.ACCESS_FINE_LOCATION};
ActivityCompat.requestPermissions(activity,p,1000);

开始搜索

开始搜索使用startScan方法即可,示例代码如下:

 bluetoothAdapter.getBluetoothLeScanner().startScan(scanCallback);

 

 public class IBeaconCallback extends ScanCallback {
 
    @Override
    public void onScanResult(int callbackType, ScanResult result) {
        super.onScanResult(callbackType, result);
        LogUtils.d("onScanResult");
        onBeaconServiceChangeCallback(true, true);
        ScanRecord scanRecord = result.getScanRecord();
        IBeaconAccept iBeaconAccept = new IBeaconAccept();
//            //测试
//            byte[] manufacturerSpecificData = scanRecord.getManufacturerSpecificData(176);
//            if (manufacturerSpecificData != null) {
//                LogUtils.d(ByteUtils.bytesToHex(manufacturerSpecificData));
//                LogUtils.d(ByteUtils.bytesToHex(scanRecord.getBytes()));
//                String s = iBeaconAccept.getIBeaconInfo(scanRecord.getBytes(), result.getRssi()).toString();
//                LogUtils.d(s);
//            }
        SparseArray<byte[]> specificData = scanRecord.getManufacturerSpecificData();
        if (specificData == null) {
            return;
        }
        for (int i = 0; i < specificData.size(); i++) {
            byte[] bytes = specificData.valueAt(i);
            if (bytes == null) {
                continue;
            }
            IBeaconAccept.IBeaconInfo iBeaconInfo = iBeaconAccept.getIBeaconInfo(scanRecord.getBytes(), result.getRssi());
            String uuid = iBeaconInfo.uuid;
            if (!TextUtils.isEmpty(uuid)) {
                for (int j = 0; j < bean.data.uuids.size(); j++) {
                    String beaconUUId = bean.data.uuids.get(j);
                    if (beaconUUId.equals(uuid) && beaconMap.get(uuid) == null) {
                        BeaconsData beaconInfoData = new BeaconsData();
                        beaconInfoData.uuid = uuid;
                        beaconInfoData.major = iBeaconInfo.major;
                        beaconInfoData.minor = iBeaconInfo.minor;
                        beaconInfoData.accuracy = iBeaconInfo.accuracy;
                        beaconInfoData.rssi = iBeaconInfo.rssi;
                        beaconInfoData.proximity = scanRecord.getTxPowerLevel();
                        beaconMap.put(uuid, beaconInfoData);
                        LogUtils.d(beaconInfoData.toString());
                        break;
                    }
                }
            }
        }
    }
}
 
public static class BeaconsData {
    public String uuid;
    public int major;
    public int minor;
    public int proximity;
    public double accuracy;
    public int rssi;
 
    @Override
    public String toString() {
        return "BeaconsData{" +
                "uuid='" + uuid + ''' +
                ", major=" + major +
                ", minor=" + minor +
                ", proximity=" + proximity +
                ", accuracy=" + accuracy +
                ", rssi=" + rssi +
                '}';
    }
}

停止搜索

BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
if (scanCallback != null && bluetoothLeScanner != null) {
    bluetoothLeScanner.stopScan(scanCallback);
    onBeaconServiceChangeCallback(false, false);
}

 

2.4 数据解析

由于iBeacon是苹果公司2013年9月发布的移动设备用OS(iOS7)上配备的新功能,Android并没有提供IBeacon的封装,这块的解析完全依据IOS来的。

public class IBeaconAccept {
 
    public IBeaconInfo getIBeaconInfo(byte[] data, int rssi) {
 
        IBeaconInfo iBeaconInfo = new IBeaconInfo();
        if (data == null) {
            return iBeaconInfo;
        }
        int startByte = 2;
        boolean patternFound = true;
        // 寻找ibeacon
        while (startByte <= 5) {
            if (((int) data[startByte + 2] & 0xff) == 0x02 && // Identifies
                    // an
                    // iBeacon
                    ((int) data[startByte + 3] & 0xff) == 0x15) { // Identifies
                // correct
                // data
                // length
                patternFound = true;
                break;
            }
            startByte++;
        }
        // 假设找到了的话
        if (patternFound) {
            // 转换为16进制
            byte[] uuidBytes = new byte[16];
            System.arraycopy(data, startByte + 4, uuidBytes, 0, 16);
            String hexString = ByteUtils.bytesToHex(uuidBytes);
 
            // ibeacon的UUID值
            String uuid = hexString.substring(0, 8) + "-"
                    + hexString.substring(8, 12) + "-"
                    + hexString.substring(12, 16) + "-"
                    + hexString.substring(16, 20) + "-"
                    + hexString.substring(20, 32);
 
            // ibeacon的Major值
            int major = (data[startByte + 20] & 0xff) * 0x100
                    + (data[startByte + 21] & 0xff);
 
            // ibeacon的Minor值
            int minor = (data[startByte + 22] & 0xff) * 0x100
                    + (data[startByte + 23] & 0xff);
 
            int txPower = (data[startByte + 24]);
            iBeaconInfo.uuid = uuid;
            iBeaconInfo.major = major;
            iBeaconInfo.minor = minor;
            iBeaconInfo.txPower = txPower;
            iBeaconInfo.rssi = rssi;
            iBeaconInfo.accuracy = calculateAccuracy(txPower, rssi);
        }
        return iBeaconInfo;
    }
 
    public double calculateAccuracy(int txPower, double rssi) {
        if (rssi == 0) {
            return -1.0;
        }
        double ratio = rssi * 1.0 / txPower;
        if (ratio < 1.0) {
            return Math.pow(ratio, 10);
        } else {
            double accuracy = (0.89976) * Math.pow(ratio, 7.7095) + 0.111;
            return accuracy;
        }
    }
 
    public static class IBeaconInfo {
 
        public String uuid;
        public int major;
        public int txPower;
        public int minor;
        public int rssi;
        public double accuracy;
 
        @Override
        public String toString() {
            return "IBeaconInfo{" +
                    "uuid='" + uuid + ''' +
                    ", major=" + major +
                    ", txPower=" + txPower +
                    ", minor=" + minor +
                    ", rssi=" + rssi +
                    ", accuracy=" + accuracy +
                    '}';
        }
    }
}

解析后的数据格式如下:

UUID:0000180d-0000-1000-8000-00805f9b34fb
Major:9
Minor:6
TxPower:-75
rssi:-47

 

三、iOS Beacon集成

iOS 在 BLE、iBeacon 开发过程中与 Android 的区别:

  • 在 iOS 中所有的数据都是通过 API 获取的,也就是说在 iOS 中不会看到蓝牙模块的裸数据(在这里的裸数据就代表蓝牙模块发送的 16 进制的数据),只能拿到苹果公司提供的极个别的 API 中的数据。
  • BLE、iBeacon 各使用各自的 API,他们之间没有任何对应关系。如果想使用 BLE 就不可能获取到 iBeacon 的 Major、Minor、UUID 等信息,如果使用 iBeacon,没有办法发起链接请求获取服务。
  • 苹果公司为了省电和隐私限制只能监听指定 UUID 的 iBeacon,无法像 Android 一样搜索附近所有的 iBeacon 信息,虽然 iBeacon 扫描底层可以获取到附近的所有的 iBeacon 信息。

Beacon 使用苹果提供CoreLocation库,然而在 BLE 在开发过程中使用 CoreBluetooth 库必须让用户点击是否允许“ XX APP 使用地理位置”。如果 在第一次使用 iOS APP 扫描 iBeacon 的时候没有提示这句话是不可能接收到 iBeacon 的信号(除非 iOS 8.0 之下)。如果是BLE则的开发过程中之需要提示用户打开蓝牙,并不要求其他的地理位置任何信息。

 

iBeacon 在 CoreLocation 框架中抽象为 CLBeacon 类, 该类有6个属性,分别是:

  • proximityUUID:是一个 NSUUID,用来标识公司。每个公司、组织使用的 iBeacon 应该拥有同样的 proximityUUID。
  • major:主要值,用来识别一组相关联的 beacon,例如在连锁超市的场景中,每个分店的 beacon 应该拥有同样的 major。
  • minor:次要值,则用来区分某个特定的 beacon。
  • proximity:远近范围的,一个枚举值,取值及说明如下:
typedef NS_ENUM(NSInteger, CLProximity) {
CLProximityUnknown,// 无效
CLProximityImmediate,//在几厘米内
CLProximityNear,//在几米内
CLProximityFar//超过 10 米以外,不过在测试中超不过10米就是far
}
  • accuracy,与iBeacon的距离。
  • rssi,信号轻度为负值,越接近0信号越强,等于0时无法获取信号强度。

 

参考链接:android-beacon-library