Android IBeacon

  • Post author:
  • Post category:其他


一、IBeacon的原理

iBeacon的工作是基于Android的低功耗蓝牙BLE,和蓝牙不同的是iBeacon不用配对和连接过程,iBeacon基站通过BLE蓝牙的广播频道不断向外发送位置信息,当智能设备进入设定区域时,不需要连接就能够收到信号,只要满足iBeacon技术标准的都可以使用。IBeacon可以通过rssi来判断设备与基站的距离,用于室内的定位、微信签到等场景。

发射端通过BLE的广告通信信道,以一定时间间隔向外广播数据包,当某个监听设备监听到这个广播数据的时候,就好发送 Scan Response Request,请求广播发送方发送扫描响应数据。这两部分数据的长度都是固定的 31 字节。在 Android 中,系统会把这两个数据拼接在一起,返回一个 62 字节的数组。

iBeacon的核心组成:

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

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

iBeacon中一般有两个角色:

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

下面分为两个部分,分别介绍:

二、发射广播信号的IBeacon


1、设置广播参数

    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();
    }

(1)setTxPowerLevel:

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

(2)setAdvertiseMode:

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

(3)setTimeout:

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

(4)setConnectable:

是否运行设备自动连接。


2、设置广播数据

    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();
    }

(1)设置广播服务的UUID

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

.addServiceUuid(parcelUuid)

addServiceUuid:设置了该参数,就可以在startScan的onScanResult中通过result.getScanRecord().getServiceUuids()获取到,否则为null。

这块的UUID是设备厂家设置的UUID信息。

(2)添加服务数据UUID和服务数据

.addServiceData(parcelUuid, “ee1”.getBytes())

addServiceData:设置了该参数,就可以在startScan的onScanResult中通过result.getScanRecord().getServiceData(parcelUuid)获取到,否则为null。

(3)广播是否包含设备名

.setIncludeDeviceName(true)

setIncludeDeviceName:设置了该参数为true,就可以在startScan的onScanResult中通过result.getScanRecord().getDeviceName()获取到蓝牙名称,否则为null。

(4)广播是否包含发射功率等级

.setIncludeTxPowerLevel(true);

setIncludeTxPowerLevel:设置了该参数为true,就可以在startScan的onScanResult中通过result.getScanRecord().getTxPowerLevel()获取到TxPower,否则为null。

(5)添加制造商ID和数据

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

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


注意

:设置的数据不能超过31个字节,否则广播发送失败,实际测试添addManufacturerData就够了,再多的话就超过了,如果想要传递其他数据addServiceUuid,addServiceData,setIncludeDeviceName等等,则需要舍弃addManufacturerData一部分。参数全都带上设置的过多会导致字节溢出。


3、制造商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接收到的数据如下:

* UUID:0000180d-0000-1000-8000-00805f9b34fb

* Major:9

* Minor:6

* TxPower:-75

* rssi:-47


4、开始广播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();
            }
        }
    };


5、停止广播iBeacon数据

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

三、接收广播信号的IBeacon


1、IBeacon权限

和BLE一样IBeacon的接收也需要系统权限。

(1)静态注册:

<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" /> 

(2)动态申请定位权限:

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

如果您的目标是 Android 10,那么您需要 ACCESS_FINE_LOCATION 来扫描。ACCESS_COARSE_LOCATION 在 Android 10 中不再起作用。本次项目targetApi为29。

(3)打开手机定位

Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);

MainActivity a = (MainActivity) activity;

a.startActivityForResult(intent, OPEN_GPS_CODE)


2、开始搜索

 bluetoothAdapter.getBluetoothLeScanner().startScan(scanCallback);
//搜索到的设备
private Map<String, BeaconsData> beaconMap = new HashMap(); 
 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 +
                '}';
    }
}


4、停止搜索

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

四、广播信号的数据解析

由于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



版权声明:本文为yoonerloop原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。