时间:2021-01-15 11:15:59 | 栏目:Android代码 | 点击:次
本文将简单介绍如何搭建一套蓝牙定位系统,供移动客户端(包括android和iOS)定位。
1、准备设备
所需硬件设备:
(1)低功率蓝牙定位器若干(如:10个),网上有卖(单价从几十到几百都有)
(2)android设备一台,系统版本4.2以上(SDK版本大于17)
(3)iOS设备一台,支持蓝牙4.0 BLE
2、设置蓝牙定位器
移动设备扫描周边低功率蓝牙设备,可以获得蓝牙设备对应的Proximity UUID、Major、Minor等属性信息。而刚采购来的蓝牙设备属性可能都相同,互相区别不开,所以我们需要设置每台设备的属性。
设备厂商都会提供相关手机应用,共用户设置属性信息。给蓝牙设备装上电池,打开手机应用,靠近蓝牙设备就能发现,然后就可以设置其属性值了,其中:
UUID是一个32位的16进制数,表示设备厂商,该字段可以沿用出厂设置
Major表示不同区域(比如:某一楼层、某一地区),取值范围0到6万多
Minor表示不同的设备,取值范围0到6万多
样例:UUID = e2c56db5-dffb-48d2-b060-d0f5a71096e0, Major = 1001, Minor = 10001
每台设备设置完属性后准备一个标签,填上属性信息,贴到设备上,方便以后部署。
3、部署蓝牙设备
首先,准备目标场地地图数据,可以是基于经纬度坐标,也可以是简单图片坐标,看具体使用情况。
接下来,将蓝牙设备挨个部署到场地指定位置上,顺便记录每个设备地理坐标或图片坐标。
最后,得到一张表格信息,记录着每台蓝牙设备属性和位置信息。这张表就是整个定位系统的指纹库,为定位算法使用。
UUID | Major | Minor | Lat | Lon |
e2c56db5-dffb-48d2-b060-d0f5a71096e0 | 1001 | 10001 | 39.45678 | 116.23456 |
e2c56db5-dffb-48d2-b060-d0f5a71096e0 | 1001 | 10002 | 39.45674 | 116.23476 |
... | ... | ... | ... | ... |
固定蓝牙设备到场地指定位置比较容易,不过记录设备坐标信息可能复杂一点,需要在地图或图片上获得相应位置点。可以开发一个App从而快速准确地记录位置信息,顺便将相关信息录入指纹库(数据库,比如:SQLite)。
部署蓝牙设备还有一个关注点就是部署间隔。低功率蓝牙设备容易受场地、环境影响,比较不稳定,所以根据场地条件每隔几米或十几米部署一台蓝牙设备。间隔太大会影响定位精度,不过太密也是资源浪费,不是越密集定位精度越高。
4、客户端App开发
客户端app主要功能就是扫描周围蓝牙设备,将设备列表信息上传定位服务器,从而获得定位效果,并展现给终端用户。
4.1 Android应用开发
工程所需SDK版本大于17。
1. App所需权限(AndroidManifest.xml文件)
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
2. 创建beacon数据项类
public class IBeaconRecord { public String address; // 设备地址(Mac) public String uuid; // Proximity UUID public int major; // Major public int minor; // Minor public int rssi; // 场强 }
其中,address属性可以不要,因为iOS设备获取不到该属性!
3. 创建扫描工具类
import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import com.example.vo.IBeaconRecord; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothManager; import android.content.Context; import android.os.Build; import android.os.Handler; public class BLEPositioning { private Context m_ctx; private Handler handler; private BluetoothManager bluetoothManager; private BluetoothAdapter mBluetoothAdapter; // 存储蓝牙扫描结果,key - name_address, value - List<IBeaconRecord> private Map<String, List<IBeaconRecord>> mapBltScanResult; public BLEPositioning(Context ctx) { super(); this.m_ctx = ctx; initParam(); } /** * 初始化 */ private void initParam() { handler = new Handler(); mapBltScanResult = new HashMap<String, List<IBeaconRecord>>(); // 设备SDK版本大于17(Build.VERSION_CODES.JELLY_BEAN_MR1)才支持BLE 4.0 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { bluetoothManager = (BluetoothManager) this.m_ctx .getSystemService(Context.BLUETOOTH_SERVICE); mBluetoothAdapter = bluetoothManager.getAdapter(); } } /** * 开始扫描蓝牙设备 */ public void startScan() { mapBltScanResult.clear(); if (mBluetoothAdapter != null && mBluetoothAdapter.isEnabled()) { // 5秒后停止扫描,毕竟扫描蓝牙设备比较费电,根据定位及时性自行调整该值 handler.postDelayed(new Runnable() { @Override public void run() { mBluetoothAdapter.stopLeScan(bltScanCallback); } }, 5 * 1000); mBluetoothAdapter.startLeScan(bltScanCallback); // 开始扫描 } } /** * 请求定位服务,由你们完成, * 如果指纹数据在本地,定位算法就在当前App里完成 */ public void requestServer() { // TODO // 利用mapBltScanResult(蓝牙扫描结果)请求定位服务或本地计算定位 } /** * 蓝牙扫描回调,获取扫描获得的蓝牙设备信息 */ private BluetoothAdapter.LeScanCallback bltScanCallback = new BluetoothAdapter.LeScanCallback() { @Override public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) { /** * 参数列表描述 * 1.device - BluetoothDevice类对象, * 通过该对象可以得到硬件地址(比如"00:11:22:AA:BB:CC")、设备名称等信息 * 2.rssi - 蓝牙设备场强值,小于0的int值 * 3.scanRecord - 这里内容比较丰富,像UUID、Major、Minor都在这里 */ IBeaconRecord record = new IBeaconRecord(); if (fromScanData(scanRecord, record)) { String address = device.getAddress(); // 获取Mac地址 String name = device.getName(); // 获取设备名称 String key = name + "_" + address; record.address = address; // Mac地址 record.rssi = rssi; // 场强 if (mapBltScanResult.containsKey(key)) { mapBltScanResult.get(key).add(record); } else { ArrayList<IBeaconRecord> list = new ArrayList<IBeaconRecord>(); list.add(record); mapBltScanResult.put(key, list); } } } }; /** * 解析蓝牙信息数据流 * 注:该段代码是从网上看到的,来源不详 * @param scanData * @param record * @return */ private boolean fromScanData(byte[] scanData, IBeaconRecord record) { int startByte = 2; boolean patternFound = false; while (startByte <= 5) { if (((int) scanData[startByte + 2] & 0xff) == 0x02 && ((int) scanData[startByte + 3] & 0xff) == 0x15) { // yes! This is an iBeacon patternFound = true; break; } else if (((int) scanData[startByte] & 0xff) == 0x2d && ((int) scanData[startByte + 1] & 0xff) == 0x24 && ((int) scanData[startByte + 2] & 0xff) == 0xbf && ((int) scanData[startByte + 3] & 0xff) == 0x16) { return false; } else if (((int) scanData[startByte] & 0xff) == 0xad && ((int) scanData[startByte + 1] & 0xff) == 0x77 && ((int) scanData[startByte + 2] & 0xff) == 0x00 && ((int) scanData[startByte + 3] & 0xff) == 0xc6) { return false; } startByte++; } if (patternFound == false) { // This is not an iBeacon return false; } // 获得Major属性 record.major = (scanData[startByte + 20] & 0xff) * 0x100 + (scanData[startByte + 21] & 0xff); // 获得Minor属性 record.minor = (scanData[startByte + 22] & 0xff) * 0x100 + (scanData[startByte + 23] & 0xff); // record.tx_power = (int) scanData[startByte + 24]; // this one is // signed // record.accuracy = calculateAccuracy(record.tx_power, record.rssi); // if (record.accuracy < 0) { // return false; // } try { byte[] proximityUuidBytes = new byte[16]; System.arraycopy(scanData, startByte + 4, proximityUuidBytes, 0, 16); String hexString = bytesToHex(proximityUuidBytes); StringBuilder sb = new StringBuilder(); sb.append(hexString.substring(0, 8)); sb.append("-"); sb.append(hexString.substring(8, 12)); sb.append("-"); sb.append(hexString.substring(12, 16)); sb.append("-"); sb.append(hexString.substring(16, 20)); sb.append("-"); sb.append(hexString.substring(20, 32)); // beacon.put("proximity_uuid", sb.toString()); // 获得UUID属性 record.uuid = sb.toString(); } catch (Exception e) { e.printStackTrace(); } return true; } private char[] hexArray = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; private String bytesToHex(byte[] bytes) { char[] hexChars = new char[bytes.length * 2]; int v; for (int j = 0; j < bytes.length; j++) { v = bytes[j] & 0xFF; hexChars[j * 2] = hexArray[v >>> 4]; hexChars[j * 2 + 1] = hexArray[v & 0x0F]; } return new String(hexChars); } }
扫描结果放在mapBltScanResult里,该HashMap的key由设备Mac地址和名称组成(address_name),value是个ArrayList,记录着该蓝牙设备多次扫描得到的信息(IBeaconRecord)序列,请求定位服务或本地计算定位之前,这些序列要进行平均处理(其实只是平均rssi值)。经过RSSI值多次平均处理后,一定程度上减小蓝牙设备不稳定因素。
关于请求定位服务,展现定位效果,还有定位算法都不是本文重点!关于蓝牙定位算法也可以参考其他文献资料!
4.2 iOS应用开发
iOS部分参考了AirLocate源码(苹果官方蓝牙样例工程)。
1. 引用基础配置类“APLDefaults”(来自AirLocate)
APLDefaults.h文件
/* File: APLDefaults.h Abstract: Contains default values for the application. Version: 1.1 Copyright (C) 2014 Apple Inc. All Rights Reserved. */ extern NSString *BeaconIdentifier; @interface APLDefaults : NSObject + (APLDefaults *)sharedDefaults; @property (nonatomic, copy, readonly) NSArray *supportedProximityUUIDs; @property (nonatomic, copy, readonly) NSUUID *defaultProximityUUID; @property (nonatomic, copy, readonly) NSNumber *defaultPower; @end
APLDefaults.m文件
/* File: APLDefaults.m Abstract: Contains default values for the application. Version: 1.1 Copyright (C) 2014 Apple Inc. All Rights Reserved. */ #import "APLDefaults.h" NSString *BeaconIdentifier = @"com.example.apple-samplecode.AirLocate"; @implementation APLDefaults - (id)init { self = [super init]; if(self) { // uuidgen should be used to generate UUIDs. _supportedProximityUUIDs = @[[[NSUUID alloc] initWithUUIDString:@"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"], [[NSUUID alloc] initWithUUIDString:@"5A4BCFCE-174E-4BAC-A814-092E77F6B7E5"], [[NSUUID alloc] initWithUUIDString:@"74278BDA-B644-4520-8F0C-720EAF059935"]]; _defaultPower = @-59; } return self; } + (APLDefaults *)sharedDefaults { static id sharedDefaults = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedDefaults = [[self alloc] init]; }); return sharedDefaults; } - (NSUUID *)defaultProximityUUID { return _supportedProximityUUIDs[0]; } @end
2. 定义变量
// 存储扫描获得的蓝牙设备信息 // key - proximityUUID_Major_Minor // value - NSArray (CLBeacon) NSMutableDictionary *dicBeacons; CLLocationManager *locationManager; NSMutableDictionary *rangedRegions; // 要扫描的region NSTimer *timerPos; // 定时器,用于控制扫描时间长短
3. 初始化
dicBeacons = [[NSMutableDictionary alloc] init]; locationManager = [[CLLocationManager alloc] init]; locationManager.delegate = self; // 当前类接收回调,从而获得蓝牙设备信息 // Populate the regions we will range once. rangedRegions = [[NSMutableDictionary alloc] init]; for (NSUUID *uuid in [APLDefaults sharedDefaults].supportedProximityUUIDs) { CLBeaconRegion *region = [[CLBeaconRegion alloc] initWithProximityUUID:uuid identifier:[uuid UUIDString]]; rangedRegions[region] = [NSArray array]; }
4. 开始扫描、停止扫描和请求定位服务
// 开始扫描蓝牙 - (void)startScanning { // 定时3.0秒后请求定位服务,时间间隔自行设置,只要有足够的扫描时间即可 timerPos = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(startPositioning) userInfo:nil repeats:NO]; [dicBeacons removeAllObjects]; // 开始扫描 for (CLBeaconRegion *region in rangedRegions) { [locationManager startRangingBeaconsInRegion:region]; } } // 停止扫描蓝牙 - (void)stopScanning { // 停止扫描 for (CLBeaconRegion *region in rangedRegions) { [locationManager stopRangingBeaconsInRegion:region]; } } // 请求定位服务 - (void)startPositioning { [self stopScanning]; // 停止扫描 // 以下根据扫描结果dicBeacons来请求定位服务 // }
其中,请求定位服务部分每个人都不一样,依赖自身定位服务。
5. 监听回调,解析扫描获得的蓝牙设备信息,存入dicBeacons变量
#pragma mark - Location manager delegate - (void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region { /* CoreLocation will call this delegate method at 1 Hz with updated range information. Beacons will be categorized and displayed by proximity. A beacon can belong to multiple regions. It will be displayed multiple times if that is the case. If that is not desired, use a set instead of an array. */ for (NSNumber *range in @[@(CLProximityUnknown), @(CLProximityImmediate), @(CLProximityNear), @(CLProximityFar)]) { NSArray *proximityBeacons = [beacons filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"proximity = %d", [range intValue]]]; for (int i = 0; i < [proximityBeacons count]; i++) { CLBeacon *beacon = [proximityBeacons objectAtIndex:i]; // 场强过滤,RSSI值要在-90到0之间 if (beacon.rssi < 0 && beacon.rssi > -90) { NSString *strKey = [NSString stringWithFormat:@"%@_%@_%@",[beacon.proximityUUID UUIDString], beacon.major, beacon.minor]; if ([dicBeacons objectForKey:strKey]) { [[dicBeacons objectForKey:strKey] addObject:beacon]; } else { NSMutableArray *arrBeacons = [[NSMutableArray alloc] init]; [arrBeacons addObject:beacon]; [dicBeacons setObject:arrBeacons forKey:strKey]; } } } } }
5. 定位服务开发
部署蓝牙设备时组建了最原始的蓝牙指纹库(数据表),利用这张表可以开发一套定位服务。
客户端上传过来的是一组蓝牙设备信息列表,例如:
{ "ble_arr” = ( { major = 1001; minor = 10006; rssi = "-65"; uuid = " E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"; }, { major = 1001; minor = 10002; rssi = "-72"; uuid = " E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"; }, { major = 1001; minor = 10005; rssi = "-49"; uuid = " E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"; }, { major = 1001; minor = 10008; rssi = "-74"; uuid = " E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"; }, { major = 1001; minor = 10001; rssi = "-65"; uuid = " E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"; }, { major = 1001; minor = 10004; rssi = "-76"; uuid = " E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"; }, { major = 1001; minor = 10007; rssi = "-66"; uuid = " E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"; }, { major = 1001; minor = 17010; rssi = "-67"; uuid = " E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"; } ); }
根据客户端上传的设备列表信息和指纹库信息计算出一个位置点返回给客户端,这样一个定位服务算搞定了!目前有多种定位算法和技术,可以参考相关文献资料!
以上就是搭建蓝牙定位系统整个内容,谢谢!