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 三点定位算法

一个信标只能告诉你「距离大概多远」。要确定具体位置,至少需要三个信标。

原理很简单:

  1. 已知三个信标的坐标 (x1,y1), (x2,y2), (x3,y3)
  2. 测出手机到三个信标的距离 d1, d2, d3
  3. 以每个信标为圆心,距离为半径画圆
  4. 三个圆的交点就是手机位置

但现实很骨感。三个圆往往不会交于一点,而是形成一个重叠区域。这时候需要用最小二乘法求近似解。

// 三点定位核心代码(简化版)
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 端,一目了然:

蓝牙定位技术知识体系 广播协议 iBeacon / Eddystone 信号采集 RSSI 扫描与滤波 距离计算 对数衰减模型 定位算法 三点定位 / 最小二乘法 优化策略 卡尔曼滤波 / 加权平均 Android 应用层 BluetoothLeScanner + 自定义 View 展示 从协议采集 → 算法计算 → 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