Beacon学习总结

2024-06-16 02:32
文章标签 学习 总结 beacon

本文主要是介绍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发送一次广播,所以如果要获取多次比较结果,就会出现单次扫描结果很慢,用户体验就会很差。

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

 

这篇关于Beacon学习总结的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1065250

相关文章

Kubernetes常用命令大全近期总结

《Kubernetes常用命令大全近期总结》Kubernetes是用于大规模部署和管理这些容器的开源软件-在希腊语中,这个词还有“舵手”或“飞行员”的意思,使用Kubernetes(有时被称为“... 目录前言Kubernetes 的工作原理为什么要使用 Kubernetes?Kubernetes常用命令总

Python中实现进度条的多种方法总结

《Python中实现进度条的多种方法总结》在Python编程中,进度条是一个非常有用的功能,它能让用户直观地了解任务的进度,提升用户体验,本文将介绍几种在Python中实现进度条的常用方法,并通过代码... 目录一、简单的打印方式二、使用tqdm库三、使用alive-progress库四、使用progres

Android数据库Room的实际使用过程总结

《Android数据库Room的实际使用过程总结》这篇文章主要给大家介绍了关于Android数据库Room的实际使用过程,详细介绍了如何创建实体类、数据访问对象(DAO)和数据库抽象类,需要的朋友可以... 目录前言一、Room的基本使用1.项目配置2.创建实体类(Entity)3.创建数据访问对象(DAO

Java向kettle8.0传递参数的方式总结

《Java向kettle8.0传递参数的方式总结》介绍了如何在Kettle中传递参数到转换和作业中,包括设置全局properties、使用TransMeta和JobMeta的parameterValu... 目录1.传递参数到转换中2.传递参数到作业中总结1.传递参数到转换中1.1. 通过设置Trans的

C# Task Cancellation使用总结

《C#TaskCancellation使用总结》本文主要介绍了在使用CancellationTokenSource取消任务时的行为,以及如何使用Task的ContinueWith方法来处理任务的延... 目录C# Task Cancellation总结1、调用cancellationTokenSource.

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]