21、蓝牙定位技术:从协议到实战
蓝牙定位,说白了就是让手机知道「我在哪」。
我最早接触这个领域是在做一个商场导购项目。客户要求:用户走到某个店铺门口,手机自动弹出优惠券。听起来简单吧?真正做起来,坑多到你想哭。
这一章,我会把蓝牙定位的底裤扒干净。从协议到算法,再到实战,一条龙讲完。
21.1 两大定位协议:iBeacon 和 Eddystone
先搞清楚一件事:蓝牙定位不是靠连接设备,而是靠「广播包」。
设备不断往外喊:「我在这里,我的信号强度是xxx」。手机收到后,根据信号强弱推算距离。
目前主流的广播协议有两个:苹果的 iBeacon 和 Google 的 Eddystone。
21.1.1 iBeacon 协议解析
iBeacon 是苹果在 iOS 7 推出的。结构很简单,就四个字段:
| 字段 | 长度 | 说明 |
|---|---|---|
| UUID | 16字节 | 应用唯一标识,比如一个商场用一个UUID |
| Major | 2字节 | 大区域,比如楼层 |
| Minor | 2字节 | 小区域,比如具体店铺 |
| Tx Power | 1字节 | 1米处的参考RSSI值 |
我习惯把 iBeacon 理解成「门牌号系统」。UUID 是小区名,Major 是楼栋号,Minor 是房间号。这样你就能精确定位到某个点。
核心要点:iBeacon 广播包中不包含坐标信息。它只告诉你「我是谁」,至于「你在哪」,需要你自己算。
21.1.2 Eddystone 协议解析
Google 在 2015 年推出了 Eddystone。它比 iBeacon 更灵活,支持多种帧类型:
- Eddystone-UID:类似 iBeacon,包含 Namespace 和 Instance
- Eddystone-URL:直接广播一个 URL,手机靠近就能打开网页
- Eddystone-TLM:广播设备自身的温度、电池等信息
- Eddystone-EID:加密广播,防止伪造
我在项目中用过 Eddystone-URL。当时客户想做一个「扫码即用」的场景,但用户懒得扫码。我们就在每个展品旁放一个 Eddystone 信标,手机靠近自动弹出展品介绍页。体验比扫码好太多。
我的建议:如果你做 iOS 为主,选 iBeacon。Android 为主,选 Eddystone。两边都要兼容?两个都支持,代码里做判断就行。
21.2 RSSI 测距原理
RSSI(Received Signal Strength Indicator)就是信号强度。单位是 dBm,范围一般在 -30 到 -100 之间。
数值越大(越接近0),说明离信标越近。数值越小(越负),说明越远。
但 RSSI 到距离的转换,不是线性的。它遵循一个对数衰减模型:
d = 10 ^ ((TxPower - RSSI) / (10 * n))
其中:
- TxPower:1米处的参考 RSSI(从广播包中获取)
- RSSI:当前接收到的信号强度
- n:环境衰减因子(通常取 2-4)
举个例子:
TxPower = -59 dBm
RSSI = -75 dBm
n = 2.0
d = 10 ^ ((-59 - (-75)) / (10 * 2))
= 10 ^ (16 / 20)
= 10 ^ 0.8
≈ 6.3 米
我曾经踩过的坑:RSSI 波动非常大。同一位置,同一设备,1秒内 RSSI 可能跳变 10 个 dBm。直接拿单次 RSSI 算距离,误差能到 5 米以上。
解决办法?滤波。我常用的方法:
- 均值滤波:取最近 5-10 个 RSSI 的平均值
- 卡尔曼滤波:适合动态场景,能平滑跟踪
- 中值滤波:去掉最大最小值,取中间值
我个人习惯用均值滤波,简单够用。如果场景复杂(比如人走来走去),上卡尔曼滤波效果更好。
21.3 三点定位算法
一个信标只能告诉你「距离大概多远」。要确定具体位置,至少需要三个信标。
原理很简单:
- 已知三个信标的坐标 (x1,y1), (x2,y2), (x3,y3)
- 测出手机到三个信标的距离 d1, d2, d3
- 以每个信标为圆心,距离为半径画圆
- 三个圆的交点就是手机位置
但现实很骨感。三个圆往往不会交于一点,而是形成一个重叠区域。这时候需要用最小二乘法求近似解。
// 三点定位核心代码(简化版)
public Point trilaterate(Point p1, double d1,
Point p2, double d2,
Point p3, double d3) {
double A = 2 * (p2.x - p1.x);
double B = 2 * (p2.y - p1.y);
double C = d1*d1 - d2*d2 + p2.x*p2.x - p1.x*p1.x + p2.y*p2.y - p1.y*p1.y;
double D = 2 * (p3.x - p2.x);
double E = 2 * (p3.y - p2.y);
double F = d2*d2 - d3*d3 + p3.x*p3.x - p2.x*p2.x + p3.y*p3.y - p2.y*p2.y;
double x = (C * E - F * B) / (A * E - D * B);
double y = (C * D - A * F) / (B * D - A * E);
return new Point(x, y);
}
注意:这段代码假设三个信标不共线。如果共线,分母会为0,直接崩掉。实际部署时一定要检查信标布局。
21.4 室内定位实战
理论讲完了,来点干货。我做一个完整的室内定位 Demo,从信标部署到 App 显示。
21.4.1 信标部署原则
信标不是随便放的。我总结了几条经验:
- 间距:信标间距 5-8 米,太密浪费,太疏定位不准
- 高度:离地 2-3 米,避免被桌椅遮挡
- 布局:尽量呈等边三角形或正方形,不要排成一条直线
- 避开金属:金属会反射蓝牙信号,导致 RSSI 异常
21.4.2 Android 端扫描实现
扫描 iBeacon 或 Eddystone,推荐用 android.bluetooth.le.BluetoothLeScanner:
BluetoothLeScanner scanner = bluetoothAdapter.getBluetoothLeScanner();
ScanFilter filter = new ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB"))
.build();
ScanSettings settings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build();
scanner.startScan(filters, settings, scanCallback);
扫描到广播包后,解析出 RSSI 和 TxPower,然后算距离:
double calculateDistance(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 {
return (0.89976) * Math.pow(ratio, 7.7095) + 0.111;
}
}
21.4.3 定位结果展示
拿到坐标后,我习惯用自定义 View 画一个平面图,把信标和手机位置标上去:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 画信标
for (Beacon b : beacons) {
canvas.drawCircle(b.x, b.y, 10, beaconPaint);
canvas.drawText(b.name, b.x, b.y - 15, textPaint);
}
// 画手机位置
if (phonePoint != null) {
canvas.drawCircle(phonePoint.x, phonePoint.y, 12, phonePaint);
}
}
实战效果:在 10m x 10m 的室内,部署 4 个信标,定位精度大约 1-3 米。如果环境空旷,精度能到 1 米以内。如果障碍物多,误差会到 3-5 米。
21.5 知识体系总览
下面这张图,把蓝牙定位的完整链路画出来了。从协议到算法,再到 App 端,一目了然:
21.6 避坑指南
最后,分享几个我踩过的坑:
- 信标电池:纽扣电池一般撑 1-2 年。电量低了,TxPower 会漂移,定位不准。建议定期巡检。
- 多路径效应:蓝牙信号会反射。在走廊、电梯口这种地方,RSSI 会异常偏高。我遇到过在走廊尽头测出 0.5 米,实际距离 5 米。
- Android 碎片化:不同手机的蓝牙芯片灵敏度不同。同一位置,小米和三星测出的 RSSI 能差 5 个 dBm。建议做设备校准。
- 扫描功耗:持续扫描很费电。我一般用
SCAN_MODE_LOW_LATENCY扫 3 秒,停 2 秒,平衡功耗和实时性。
我曾经踩过的一个大坑:在商场部署了 50 个信标,结果发现某些区域手机能扫到 20 多个信标。扫描回调太频繁,导致 UI 线程卡死。后来加了去重和限流,只处理信号最强的 5 个信标。
蓝牙定位,说白了就是「用信号强度猜距离,用距离算位置」。精度不可能像 GPS 那么高,但胜在室内能用、成本低。如果你做的是「用户走到某个区域触发事件」这种场景,蓝牙定位完全够用。
好了,这一章就到这里。代码和思路都给你了,剩下的就是动手去试。
公众号:蓝海资料掘金营,微信deep3321