Beacon学习总结

最近接触一个项目是关于室内定位的,利用的是蓝牙定位技术,主要涉及到BLE编程,以及对Beacon这种蓝牙基站的认识。

我们项目使用的Beacon基站是由国内一家产品公司Voliam科技开发的vBeacon。由于Voliam公司并没向我们团队提供SDK或开发文档,只是给我们提供了两个APK,(一个是Demo,另一个是他们官方的配置软件)

分别是VoliamBeaconDemo.apk 和iBeaconTool.apk(配置软件)

由于一开始接触的是IBeacon,是苹果在2013年发布的一项技术,刚发布就掀起了热议,业界认为开发前景十分大,甚至会将焦点都转向它,围绕它发起一场大革命,

然而在网上搜索之后发现,时隔两年这项技术却并没有多少成功开发的案例,腾讯微信最近一次更新就是在摇一摇中添加了周边的功能,实质就是使用了beacon技术。

在使用beacon做较精准的室内定位这一项,目前似乎还没有成功案例。唯一搜到与beacon定位比较相关的是Estimote最新的sdk就是支持室内定位的。

http://36kr.com/p/215699.html

由于使用estimote、 sensoro 官方提供的sdk demo 都不能搜索到vBeacon的信息,而Voliam官方的demo却能搜索到vBeacon,于是 我决定先探一探Voliam官方的demo是如何运作的,想办法看看它的源码。我便决定反编译voliam官方提供的demo APK,分析源码后便可以自己做一个beacon库了。

     于是便开始了反编译。

PART1反编译相关

反编译其实很简单,因为现在有牛人帮我们做好了傻瓜式的一键反编译软件,在百度上可搜到下载链接,这里不再贴出

     我用的反编译工具是ApkDec + jd-gui.

     ApkDec是一个一键反编译软件,用以将apk文件反编程生成成jar包、 apk包含的res文件和smali文件(由安卓中的Java虚拟机(Dalvik)汇编之后形成的文件)

     jd-gui(java decompiler-gui)则提供一个图形化界面查看.class文件中的java源码。

     反编译之后发现iBeaconTool 设置了加锁算法,生成的jar包中的类和变量都是由a-z命名的,没法阅读。而VoliamBeaconDemo没有设置加锁算法,所以源码基本完整。(有一部分语法错误和语句缺失,如内部类会变成含”$”的语句)。

     于是决定着重研究VoliamBeaconDemo的源码。

看过之后发现,该apk实质上还是用的Estimote的SDK,只是将estimote的SDK改动一些代码,就拿来用而已。Voliam在原SDK中的源码中改变了什么使得新的sdk能检测到vBeacon呢?这就是我们如何将estimote的sdk变成自己项目中能用的sdk的关键了。

PART2 Estimote源码研究  (待改进)

看过一遍代码后,发现重点在BeaconManager类和BeaconService类中,

     BeaconManager类主要是整合estimote的beacon库,向外提供使用beacon库的接口

     BeaconService类主要是完成具体的BLE(Bluetooth Low Energy)设备的搜索工作。(Beacon是一种符合ble标准的蓝牙基站)

我们先来看看estimote的外调程序的入口,从estimote官方sdk帮助文档可看到,github链接:

https://github.com/Estimote/Android-SDK

分别是:

beaconManager.setOnRangingListener()//设置搜索监听器、beaconManager.startRanging()//开始搜索设备beaconManager.connect()//开始建立连接

查看sdk中BeaconManager源码可知,BeaconManager定义了一个RangingListener回调接口,接口中有一个回调方法,onBeaconDiscovered(),当搜索到Beacon设备后,beaconManager会调用onBeaconDiscovered()方法,从而将beacon设备的信息传给外部调用者。

在connect()方法中,会调用bindService()方法,将beaconManager和BeaconService绑定在一起,

bindService()其中有一个参数是ServiceConnection对象à

ServiceConnection#onServiceConnected()主要是做一些扫描前的初始化以及回调onServiceReady()方法

于是转到BeaconService类

首先看onBind()方法,onBind()方法主要是取得由BeaconManager对象中传入的Messenger对象,再看onCreate()方法à

     OnCreate()方法,主要是创建广播接听者并注册它们,并且将”startScan”和”stopScan” intent封装为pendingIntent。

this.bluetoothBroadcastReceiver= createBluetoothBroadcastReceiver();

this.scanStartBroadcastReceiver =createScanStartBroadcastReceiver();

this.afterScanBroadcastReceiver= createAfterScanBroadcastReceiver();

registerReceiver(this.bluetoothBroadcastReceiver,new IntentFilter("android.bluetooth.adapter.action.STATE_CHANGED"));

registerReceiver(this.scanStartBroadcastReceiver,new IntentFilter("startScan"));

registerReceiver(this.afterScanBroadcastReceiver,new IntentFilter("afterScan"));

this.afterScanBroadcastPendingIntent= PendingIntent.getBroadcast(getApplicationContext(), 0, AFTER_SCAN_INTENT, 0);

this.scanStartBroadcastPendingIntent= PendingIntent.getBroadcast(getApplicationContext(), 0, SCAN_START_INTENT, 0);

又在#handle中,获取到BeaconManager传入的参数,为(startRanging),于是执行startRanging()方法

在startRanging()中调用了startScanning()

在startScanning()中调用了BluetoothAdapter#startLeScan()方法 ,到此beaconService调用bluetoothAdapter开始进行扫描。

startLeScan()方法,传入一个LeScanCallback对象作为参数

重写LeScanCallback#onLeScan(BluetoothDevicedevice,int rssi,byte[])方法,便可获取到ble设备了,再将ble设备转换为beacon设备,就可获取到beacon设备信息了。

于是我们来重点看一下estimote sdk 中重写的onLeScan方法,

public voidonLeScan(BluetoothDevice device, int rssi, byte[] scanRecord)

{

      Beacon beacon =Utils.beaconFromLeScan(device, rssi, scanRecord);

      if ((beacon == null) ||(!(EstimoteBeacons.isEstimoteBeacon(beacon)))) {

        L.v("Device " + device +" is not an Estimote beacon");

        return;

      }

     BeaconService.this.beaconsFoundInScanCycle.put(beacon, Long.valueOf(System.currentTimeMillis()));

}

这里!(EstimoteBeacons.isEstimoteBeacon(beacon))是Estimote sdk中对beacon设备做了一次过滤,也就是说,这里将不符合Estimote规范的beacon设备都过滤掉,不获取不符合规范的beacon,所以如果我们要利用estimote sdk,使estimote的sdk能获取到我们自己的beacon的话,只需要修改EstimoteBeacons类中的isEstimoteBeacon(beacon)方法

转到EstimoteBeacons类,查到isEstimoteBeacon(beacon)方法,发现果然Estimote 将自己的UUID和命名规范作为识别器去过滤掉其他的beacon设备,在这里我们只需把自己项目中使用的uuid加入判断,就可以了。

然而因为jar包中反编译出来有一部分代码是缺失的,所以我们要给jar包添加东西,只能将编译后的class文件覆盖掉原class文件,(需保证覆盖前后的两个类的包名是完全相同的)然后再把所有的class文件重新打包成jar包,就成了我们自制的sdk了。因为这个过程比较繁琐,操作起来不方便,所以我们要保证项目中尽量少出现要修改sdk的情况(最好不要出现),所以打包库的人就应该把所有项目中需要使用到的api都考虑到,并打包好成一个lib库,让开发者与sdk完全隔断,只需调用你打包好的库就OK。

我们加入自己的UUID之后,就能识别自己的Beacon设备了,但是这就OK了么?如果之后项目决定不再用这家公司的beacon,换了另一家beacon,或者每次修改beacon的UUID之后(后面会讲到可以修改beacon的参数)那是不是还要再次反编译jar,把自己的UUID加进去呢?可是如果不这样,那要怎么识别我们自家的beacon呢?(定位时可能会出现有其他beacon的情况,如何排除掉?)

很简单,我们只需要将识别beacon的步骤放到自己打包的lib中,而不是放在sdk中,这样就可以避免繁琐而且不合理的修改sdk的步骤了。

于是我们将isEstimoteBeacon(beacon)的方法体定义为

{        

return true;

}

这样就可以获取到所有的beacon。我们再来看看这个beacon会传到什么地方去,

BeaconService.this.beaconsFoundInScanCycle.put(beacon,Long.valueOf(System.currentTimeMillis()));

 

this.afterScanBroadcastPendingIntent =PendingIntent.getBroadcast(getApplicationContext(), 0, AFTER_SCAN_INTENT, 0);

 

onCreate()中

registerReceiver(this.afterScanBroadcastReceiver,new IntentFilter("afterScan"));

startScanning()中

setAlarm(this.afterScanBroadcastPendingIntent,scanPeriodTimeMillis());

afterScanBroadcastReceiver.onReceive()

BeaconService.this.handler.post(BeaconService.this.afterScanCycleTask);

afterScanCycleTask中

     有processRanging()   

 processRanging()

{       for (RangingRegion rangedRegion :BeaconService.this.rangedRegions)

rangedRegion.processFoundBeacons(BeaconService.this.beaconsFoundInScanCycle);

    }

rangedRegion.processFoundBeacons(BeaconService.this.beaconsFoundInScanCycle);

和invokeCallbacks(enteredRegions,exitedRegions);

 invokeCallbacks()

for(RangingRegion rangingRegion : BeaconService.this.rangedRegions) {

        try {

MessagerangingResponseMsg = Message.obtain(null, 3);  

rangingResponseMsg.getData().putParcelable("rangingResult",new RangingResult(rangingRegion.region, rangingRegion.getSortedBeacons()));

         rangingRegion.replyTo.send(rangingResponseMsg);

}…

转到BeaconManager.handler有

Switch(msg.what)

case 3:

if(BeaconManager.this.rangingListener == null) return;      

msg.getData().setClassLoader(RangingResult.class.getClassLoader());

RangingResultrangingResult = (RangingResult)msg.getData().getParcelable("rangingResult");

BeaconManager.this.rangingListener.onBeaconsDiscovered(rangingResult.region,rangingResult.beacons);


到此,可知beacon传到了onBeaconsDiscovered()中,知道了这个,我们便可以在onBeaconsDiscovered()中对搜索到的beacon做过滤。

PART3 Beacon连接相关,BLE编程

上面说到可以修改beacon设备的UUID,但是要如何修改呢?

我们看源码

在beaconService.LeScanCallback 的onLeScan(device,rssi,byte[])方法中,

将ble device转换为beacon 的关键代码是

Beacon beacon = Utils.beaconFromLeScan(device, rssi, scanRecord);

于是看Utils类:beaconFromLeScan(device,rssi, scanRecord)方法

String scanRecordAsHex = HashCode.fromBytes(scanRecord).toString();

String proximityUUID = String.format("%s-%s-%s-%s-%s", newObject[] { scanRecordAsHex.substring(18, 26), scanRecordAsHex.substring(26,30), scanRecordAsHex.substring(30, 34), scanRecordAsHex.substring(34, 38),scanRecordAsHex.substring(38, 50) });

可知uuid的格式是8-4-4-4-12

int major = unsignedByteToInt(scanRecord[(i + 22)]) * 256 +unsignedByteToInt(scanRecord[(i + 23)]);

int minor = unsignedByteToInt(scanRecord[(i + 24)]) * 256 +unsignedByteToInt(scanRecord[(i + 25)]);

int measuredPower = scanRecord[(i + 26)];

这里可知我们通过onLeScan扫描到的beacons(ble设备),只能单方面获取到它们的广播的数据,如uuid,major(区域识别ID),minor(组识别id)(用于管理) 和measuredPower(即txPower)表示一米时的rssi值,用于测距。然而如果要获得更多的数据(如beacon的电池量,发射功率等),以及要修改它的参数,就必须和beacon设备建立连接,这里就涉及到Google推出的BLE(Bluetooth Low Energy)编程(由于beacon都符合ble规范),请花上十几分钟的时间看一下google的官方文档

https://developer.android.com/guide/topics/connectivity/bluetooth-le.html

由于Google经常被墙,所以上不去的同学可以看看

http://ricardoli.com/2014/07/31/%E8%93%9D%E7%89%9940%E2%80%94%E2%80%94android-ble%E5%BC%80%E5%8F%91%E5%AE%98%E6%96%B9%E6%96%87%E6%A1%A3%E7%BF%BB%E8%AF%91/

或是

http://my.oschina.net/tingzi/blog/215008

简单地说明一下就是一个ble设备上有一个Gatt(Generic Attribute Profile)服务端,该profile存储该设备的所有属性,而profile又由一个或多个Service组成,一个Service由一个或多个charcteristic组成,每个characteristic包含一组数据以及一个或多个descriptor

Profileßservicesßcharacteristicsßvalues & descriptors

也就是说最终是由characteristic来存储ble设备的各个属性,

而要获得各个characteristic ,则需先跟ble设备上的GATT服务端建立连接,需要使用connectGatt()方法

connectGatt方法传递三个参数:

Context对象, boolean值(表示当ble设备可用时是否自动连接),和BluetoothGattCallback调用,返回一个BluetoothGatt实例

代码如

mBluetoothGatt = device.conectGatt(mContext,false,mGattCallback)

 

BluetoothGattCallback中有几个需要覆盖的方法:

onConnectionStateChange(BluetoothGattgatt,int status,int newState)//当连接状态发生改变时调用

onServicesDiscovered(BluetoothGattgatt,int status)//发现新服务端

onCharacteristicRead(BluetoothGattgatt,BluetoothGattCharacteristic characteristic,int status)//读属性

onCharacteristicWrite(BluetoothGattgatt,BluetoothGattCharacteristic characteristic,int status)//写属性

……

于是在onServicesDiscovered()方法中,我们便可以获得gatt server中的所有characteristic

然而由于characteristic的标识符是uuid 而存的数据都是byte[]

所以要想知道beacon的哪个参数 对应哪个characteristic,(博主曾经试过将所有characteristic列举出来后一个一个去取值匹配,结果不仅把自己整残了,最后还是整不出来=。=)

这个时候就需要你跟硬件产品公司打交道了,一般买产品之后会有客服电话,直接询问客服或他们公司的技术人员是最行之有效的方法,找他们要“所有Characteristic和Service的UUID”,如果这家产品公司连这个也没法提供,要么你们项目放弃连接服务端这个功能(beacon设备的配置功能),要么换一家产品公司(更推荐) 。

 

于是,通过传递自定义的BluetoothGattCallback,实现对连接过程(开始建立到关闭连接)的具体操作,有了各个有效的characteristic的UUID之后,还有一点要注意的是,在onServicesDiscovered()方法获得的各个characteristic,这个时候你如果取得characteristic对象后用getValue()方法,获取到的值都是null,想要获得各个属性值,还需要调用BluetoothGatt实例的readCharacteristic(characteristic)方法。

每调用一次readCharacteristic(),就是相应地回调一次onCharacteristicRead()方法,于是最后是在onCharacteristicRead()方法中获得特征值的。

注意:这里的” 有效”指,有存放beacon参数的characteristic

 

关于与gatt建立连接的实现细节以及修改参数这部分就先不说了,容我再整理一下再发上来。

 

PART4 测距相关:

测距实质都是根据实时的RSSI和txPower的值去计算手机距离基站的距离。

看VoliamBeaconDemo反编译后的源码可知,Estimote的测距算法放在Utils类中的computeAccuracy(Beaconbeacon)方法中

我们来看看computeAccuracy()

public static doublecomputeAccuracy(Beacon beacon) {

    if (beacon.getRssi() == 0) {

      return -1.0D;

}

double ratio = beacon.getRssi() / beacon.getMeasuredPower();

   double rssiCorrection = 0.96D + Math. pow(Math.abs(beacon.getRssi()),3.0D) % 10.0D / 150.0D;

    if (ratio <= 1.0D) {

      return (Math.pow(ratio, 9.98D) *rssiCorrection);

    }

    return ((0.103D + 0.89978D *Math.pow(ratio, 7.71D)) * rssiCorrection);

  }

因为computeAccuracy()方法是外部调用方法,所以我们要想用自己的测距算法的话,可以直接在工程里面建一个距离工具类,专门负责距离相关。可以把estimote的算法封装成算法1,再把其他算法封装为算法2,3,4…使用策略模式,把算法作为一个参数传入到测距方法中,就可以使用不同的算法测距了。

测距算法在网上能搜到,不过公式都差不多,就是公式中的几个参数有所改变,某款手机可能使用estimote的算法更加精准,而其他手机可能用其他算法更精准。

在Github上有人创建了一个开源库ALTBeaconhttps://github.com/AltBeacon/android-beacon-library

为了使ibeacon在android端用起来跟在ios端一样方便。

altBeacon使用的算法实质还是使用了跟estimote的测距算法一样的公式,只是altBeacon搭建了一个服务端,存放了最适合各个机型的参数列表,各个手机从服务端获取最适配自己的公式参数,再传递给测距方法,理论上就可以获得最精准的距离。

但是我们项目要求能在多个基站中识别出最近的那个基站,所以对测距算法的要求会更高。试过altBeacon发现精度并不高(对两个离得较近的beacon识别出最近的那个的情况)

于是考虑可否通过多次测量来减少单次误差

获取每次的minBeacon, 将所有的minBeacon存入一个map中,然后获取次数最多的minBeacon

但是由于Beacon是1s发送一次广播,所以如果要获取多次比较结果,就会出现单次扫描结果很慢,用户体验就会很差。

如何从速度和精度直接取平衡?

 

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值