一、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