搭建簡(jiǎn)易藍(lán)牙定位系統(tǒng)的實(shí)現(xiàn)方法
本文將簡(jiǎn)單介紹如何搭建一套藍(lán)牙定位系統(tǒng),供移動(dòng)客戶端(包括android和iOS)定位。
1、準(zhǔn)備設(shè)備
所需硬件設(shè)備:
(1)低功率藍(lán)牙定位器若干(如:10個(gè)),網(wǎng)上有賣(單價(jià)從幾十到幾百都有)
(2)android設(shè)備一臺(tái),系統(tǒng)版本4.2以上(SDK版本大于17)
(3)iOS設(shè)備一臺(tái),支持藍(lán)牙4.0 BLE
2、設(shè)置藍(lán)牙定位器
移動(dòng)設(shè)備掃描周邊低功率藍(lán)牙設(shè)備,可以獲得藍(lán)牙設(shè)備對(duì)應(yīng)的Proximity UUID、Major、Minor等屬性信息。而剛采購(gòu)來的藍(lán)牙設(shè)備屬性可能都相同,互相區(qū)別不開,所以我們需要設(shè)置每臺(tái)設(shè)備的屬性。
設(shè)備廠商都會(huì)提供相關(guān)手機(jī)應(yīng)用,共用戶設(shè)置屬性信息。給藍(lán)牙設(shè)備裝上電池,打開手機(jī)應(yīng)用,靠近藍(lán)牙設(shè)備就能發(fā)現(xiàn),然后就可以設(shè)置其屬性值了,其中:
UUID是一個(gè)32位的16進(jìn)制數(shù),表示設(shè)備廠商,該字段可以沿用出廠設(shè)置
Major表示不同區(qū)域(比如:某一樓層、某一地區(qū)),取值范圍0到6萬多
Minor表示不同的設(shè)備,取值范圍0到6萬多
樣例:UUID = e2c56db5-dffb-48d2-b060-d0f5a71096e0, Major = 1001, Minor = 10001
每臺(tái)設(shè)備設(shè)置完屬性后準(zhǔn)備一個(gè)標(biāo)簽,填上屬性信息,貼到設(shè)備上,方便以后部署。
3、部署藍(lán)牙設(shè)備
首先,準(zhǔn)備目標(biāo)場(chǎng)地地圖數(shù)據(jù),可以是基于經(jīng)緯度坐標(biāo),也可以是簡(jiǎn)單圖片坐標(biāo),看具體使用情況。
接下來,將藍(lán)牙設(shè)備挨個(gè)部署到場(chǎng)地指定位置上,順便記錄每個(gè)設(shè)備地理坐標(biāo)或圖片坐標(biāo)。
最后,得到一張表格信息,記錄著每臺(tái)藍(lán)牙設(shè)備屬性和位置信息。這張表就是整個(gè)定位系統(tǒng)的指紋庫(kù),為定位算法使用。
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 |
... | ... | ... | ... | ... |
固定藍(lán)牙設(shè)備到場(chǎng)地指定位置比較容易,不過記錄設(shè)備坐標(biāo)信息可能復(fù)雜一點(diǎn),需要在地圖或圖片上獲得相應(yīng)位置點(diǎn)??梢蚤_發(fā)一個(gè)App從而快速準(zhǔn)確地記錄位置信息,順便將相關(guān)信息錄入指紋庫(kù)(數(shù)據(jù)庫(kù),比如:SQLite)。
部署藍(lán)牙設(shè)備還有一個(gè)關(guān)注點(diǎn)就是部署間隔。低功率藍(lán)牙設(shè)備容易受場(chǎng)地、環(huán)境影響,比較不穩(wěn)定,所以根據(jù)場(chǎng)地條件每隔幾米或十幾米部署一臺(tái)藍(lán)牙設(shè)備。間隔太大會(huì)影響定位精度,不過太密也是資源浪費(fèi),不是越密集定位精度越高。
4、客戶端App開發(fā)
客戶端app主要功能就是掃描周圍藍(lán)牙設(shè)備,將設(shè)備列表信息上傳定位服務(wù)器,從而獲得定位效果,并展現(xiàn)給終端用戶。
4.1 Android應(yīng)用開發(fā)
工程所需SDK版本大于17。
1. App所需權(quán)限(AndroidManifest.xml文件)
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
2. 創(chuàng)建beacon數(shù)據(jù)項(xiàng)類
public class IBeaconRecord { public String address; // 設(shè)備地址(Mac) public String uuid; // Proximity UUID public int major; // Major public int minor; // Minor public int rssi; // 場(chǎng)強(qiáng) }
其中,address屬性可以不要,因?yàn)閕OS設(shè)備獲取不到該屬性!
3. 創(chuàng)建掃描工具類
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; // 存儲(chǔ)藍(lán)牙掃描結(jié)果,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>>(); // 設(shè)備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(); } } /** * 開始掃描藍(lán)牙設(shè)備 */ public void startScan() { mapBltScanResult.clear(); if (mBluetoothAdapter != null && mBluetoothAdapter.isEnabled()) { // 5秒后停止掃描,畢竟掃描藍(lán)牙設(shè)備比較費(fèi)電,根據(jù)定位及時(shí)性自行調(diào)整該值 handler.postDelayed(new Runnable() { @Override public void run() { mBluetoothAdapter.stopLeScan(bltScanCallback); } }, 5 * 1000); mBluetoothAdapter.startLeScan(bltScanCallback); // 開始掃描 } } /** * 請(qǐng)求定位服務(wù),由你們完成, * 如果指紋數(shù)據(jù)在本地,定位算法就在當(dāng)前App里完成 */ public void requestServer() { // TODO // 利用mapBltScanResult(藍(lán)牙掃描結(jié)果)請(qǐng)求定位服務(wù)或本地計(jì)算定位 } /** * 藍(lán)牙掃描回調(diào),獲取掃描獲得的藍(lán)牙設(shè)備信息 */ private BluetoothAdapter.LeScanCallback bltScanCallback = new BluetoothAdapter.LeScanCallback() { @Override public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) { /** * 參數(shù)列表描述 * 1.device - BluetoothDevice類對(duì)象, * 通過該對(duì)象可以得到硬件地址(比如"00:11:22:AA:BB:CC")、設(shè)備名稱等信息 * 2.rssi - 藍(lán)牙設(shè)備場(chǎng)強(qiáng)值,小于0的int值 * 3.scanRecord - 這里內(nèi)容比較豐富,像UUID、Major、Minor都在這里 */ IBeaconRecord record = new IBeaconRecord(); if (fromScanData(scanRecord, record)) { String address = device.getAddress(); // 獲取Mac地址 String name = device.getName(); // 獲取設(shè)備名稱 String key = name + "_" + address; record.address = address; // Mac地址 record.rssi = rssi; // 場(chǎng)強(qiáng) if (mapBltScanResult.containsKey(key)) { mapBltScanResult.get(key).add(record); } else { ArrayList<IBeaconRecord> list = new ArrayList<IBeaconRecord>(); list.add(record); mapBltScanResult.put(key, list); } } } }; /** * 解析藍(lán)牙信息數(shù)據(jù)流 * 注:該段代碼是從網(wǎng)上看到的,來源不詳 * @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); } }
掃描結(jié)果放在mapBltScanResult里,該HashMap的key由設(shè)備Mac地址和名稱組成(address_name),value是個(gè)ArrayList,記錄著該藍(lán)牙設(shè)備多次掃描得到的信息(IBeaconRecord)序列,請(qǐng)求定位服務(wù)或本地計(jì)算定位之前,這些序列要進(jìn)行平均處理(其實(shí)只是平均rssi值)。經(jīng)過RSSI值多次平均處理后,一定程度上減小藍(lán)牙設(shè)備不穩(wěn)定因素。
關(guān)于請(qǐng)求定位服務(wù),展現(xiàn)定位效果,還有定位算法都不是本文重點(diǎn)!關(guān)于藍(lán)牙定位算法也可以參考其他文獻(xiàn)資料!
4.2 iOS應(yīng)用開發(fā)
iOS部分參考了AirLocate源碼(蘋果官方藍(lán)牙樣例工程)。
1. 引用基礎(chǔ)配置類“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. 定義變量
// 存儲(chǔ)掃描獲得的藍(lán)牙設(shè)備信息 // key - proximityUUID_Major_Minor // value - NSArray (CLBeacon) NSMutableDictionary *dicBeacons; CLLocationManager *locationManager; NSMutableDictionary *rangedRegions; // 要掃描的region NSTimer *timerPos; // 定時(shí)器,用于控制掃描時(shí)間長(zhǎng)短
3. 初始化
dicBeacons = [[NSMutableDictionary alloc] init]; locationManager = [[CLLocationManager alloc] init]; locationManager.delegate = self; // 當(dāng)前類接收回調(diào),從而獲得藍(lán)牙設(shè)備信息 // 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. 開始掃描、停止掃描和請(qǐng)求定位服務(wù)
// 開始掃描藍(lán)牙 - (void)startScanning { // 定時(shí)3.0秒后請(qǐng)求定位服務(wù),時(shí)間間隔自行設(shè)置,只要有足夠的掃描時(shí)間即可 timerPos = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(startPositioning) userInfo:nil repeats:NO]; [dicBeacons removeAllObjects]; // 開始掃描 for (CLBeaconRegion *region in rangedRegions) { [locationManager startRangingBeaconsInRegion:region]; } } // 停止掃描藍(lán)牙 - (void)stopScanning { // 停止掃描 for (CLBeaconRegion *region in rangedRegions) { [locationManager stopRangingBeaconsInRegion:region]; } } // 請(qǐng)求定位服務(wù) - (void)startPositioning { [self stopScanning]; // 停止掃描 // 以下根據(jù)掃描結(jié)果dicBeacons來請(qǐng)求定位服務(wù) // }
其中,請(qǐng)求定位服務(wù)部分每個(gè)人都不一樣,依賴自身定位服務(wù)。
5. 監(jiān)聽回調(diào),解析掃描獲得的藍(lán)牙設(shè)備信息,存入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]; // 場(chǎng)強(qiáng)過濾,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. 定位服務(wù)開發(fā)
部署藍(lán)牙設(shè)備時(shí)組建了最原始的藍(lán)牙指紋庫(kù)(數(shù)據(jù)表),利用這張表可以開發(fā)一套定位服務(wù)。
客戶端上傳過來的是一組藍(lán)牙設(shè)備信息列表,例如:
{ "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"; } ); }
根據(jù)客戶端上傳的設(shè)備列表信息和指紋庫(kù)信息計(jì)算出一個(gè)位置點(diǎn)返回給客戶端,這樣一個(gè)定位服務(wù)算搞定了!目前有多種定位算法和技術(shù),可以參考相關(guān)文獻(xiàn)資料!
以上就是搭建藍(lán)牙定位系統(tǒng)整個(gè)內(nèi)容,謝謝!
這篇搭建簡(jiǎn)易藍(lán)牙定位系統(tǒng)的實(shí)現(xiàn)方法就是小編分享給大家的全部?jī)?nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Android實(shí)現(xiàn)屏蔽微信拉黑和刪除聯(lián)系人功能示例
本篇文章主要介紹了Android實(shí)現(xiàn)屏蔽微信拉黑和刪除聯(lián)系人功能示例,具有一定的參考價(jià)值,有興趣的可以了解一下。2017-02-02Android viewpager在最后一頁滑動(dòng)之后跳轉(zhuǎn)到主頁面的實(shí)例代碼
這篇文章主要介紹了Android viewpager在最后一頁滑動(dòng)之后跳轉(zhuǎn)到主頁面的實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2016-08-08Android模擬實(shí)現(xiàn)華為系統(tǒng)升級(jí)進(jìn)度條
這篇文章主要介紹了如何通過Android模擬實(shí)現(xiàn)華為在系統(tǒng)升級(jí)時(shí)顯示的進(jìn)度條。文中的實(shí)現(xiàn)過程講解詳細(xì),感興趣的小伙伴可以動(dòng)手試一試2022-01-01詳解Android studio實(shí)現(xiàn)語音轉(zhuǎn)文字功能
這篇文章主要介紹了如何通過Android studio調(diào)用科大訊飛的語音轉(zhuǎn)文字功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2022-03-03Android金額輸入框只允許輸入小數(shù)點(diǎn)后兩位效果
實(shí)現(xiàn)android 金額輸入框輸入小數(shù)點(diǎn)后兩位的效果也不是很復(fù)雜,只需要設(shè)置輸入框輸入的字符類型、設(shè)置InputFilter、設(shè)置輸入變化監(jiān)聽即可。這篇文章主要介紹了Android金額輸入框只允許輸入小數(shù)點(diǎn)后兩位 ,需要的朋友可以參考下2017-05-05android studio 4.0 新建類沒有修飾符的方法
這篇文章主要介紹了android studio 4.0 新建類沒有修飾符的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10解決Android 6.0獲取wifi Mac地址為02:00:00:00:00:00問題
這篇文章主要介紹了Android 6.0獲取wifi Mac地址為02:00:00:00:00:00的解決方法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-11-11Android自定義實(shí)現(xiàn)可滑動(dòng)按鈕
這篇文章主要為大家詳細(xì)介紹了Android自定義實(shí)現(xiàn)可滑動(dòng)的按鈕,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-01-01Android ScrollView實(shí)現(xiàn)下拉彈回動(dòng)畫效果
這篇文章主要為大家詳細(xì)介紹了Android ScrollView實(shí)現(xiàn)下拉彈回動(dòng)畫效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08如何通過Battery Historian分析Android APP耗電情況
Android 從兩個(gè)層面統(tǒng)計(jì)電量的消耗,分別為軟件排行榜及硬件排行榜。它們各有自己的耗電榜單,軟件排行榜為機(jī)器中每個(gè) App 的耗電榜單,硬件排行榜則為各個(gè)硬件的耗電榜單。這兩個(gè)排行榜的統(tǒng)計(jì)是互為獨(dú)立,互不干擾的2021-06-06