蓝牙定位标签实战:Beacon广播、RSSI采集、围栏触发、后台扫描

各位同学,今天我们来聊一个非常实战的话题——蓝牙定位标签。说白了,就是让手机知道“你离某个蓝牙设备有多近”,然后根据距离做点事情。

我在做第一个BLE定位项目时,踩了不少坑。比如RSSI值忽高忽低,围栏动不动就误触发。嗯,今天我把这些经验都揉进这节课里,你学完就能直接上手。

1. Beacon广播:定位的“灯塔”

Beacon本质上就是一个不停广播的BLE设备。它不连接,只发数据包。手机端收到广播包,就能算出距离。

一个标准的iBeacon广播包,核心字段就这几个:

字段 长度 说明
UUID 16字节 标识组织/应用
Major 2字节 标识区域(如楼层)
Minor 2字节 标识具体设备(如工位)
Tx Power 1字节 1米处的参考RSSI值

我个人习惯把UUID当作“项目ID”,Major当作“楼层”,Minor当作“具体点位”。这样后期维护起来特别清晰。

核心要点:Beacon不建立连接,只发广播。手机端通过扫描广播包来定位。

2. RSSI采集:距离的“温度计”

RSSI就是信号强度。数值越大(越接近0),说明离得越近。但RSSI有个毛病——它特别不稳定。

为什么会这样?因为蓝牙信号在空气中会反射、吸收、干扰。你拿着手机原地转一圈,RSSI能跳10个dBm。

我在项目中遇到过这种情况:设备明明没动,RSSI却从-60跳到了-75。如果直接用原始值做判断,围栏会疯掉。

解决方案是滤波。我常用的方法有两种:

  • 均值滤波:取最近N个RSSI值的平均值。N一般取5~10。
  • 卡尔曼滤波:更平滑,但计算量稍大。适合对实时性要求高的场景。
// 均值滤波示例
fun filterRssi(rssiList: List<Int>): Int {
    if (rssiList.isEmpty()) return -100
    return rssiList.average().toInt()
}

// 卡尔曼滤波(简化版)
class KalmanFilter(private val processNoise: Float = 0.01f) {
    private var estimate = -70f
    private var errorCovariance = 1f

    fun update(measurement: Float): Float {
        // 预测
        errorCovariance += processNoise
        // 更新
        val kalmanGain = errorCovariance / (errorCovariance + 0.1f)
        estimate += kalmanGain * (measurement - estimate)
        errorCovariance *= (1 - kalmanGain)
        return estimate
    }
}
我的建议:如果只是做简单的围栏触发,均值滤波就够了。别把问题搞复杂。

3. 围栏触发:距离的“警戒线”

围栏就是设定一个距离阈值。比如:当RSSI大于-65时,认为用户进入了围栏区域。

但直接比较RSSI值有个坑——不同手机的蓝牙天线灵敏度不一样。同一台Beacon,iPhone测出来-60,小米可能测出来-70。

我曾经因为这个被客户投诉过。后来我改用“距离估算”代替RSSI直接比较。

距离估算公式:

distance = 10 ^ ((txPower - rssi) / (10 * n))

其中:

  • txPower:1米处的参考RSSI(从广播包中获取)
  • rssi:当前测得的信号强度
  • n:环境衰减因子(一般取2~4)

这样算出来的距离,在不同手机上相对稳定。嗯,这里要注意:n值需要根据实际环境调试。空旷环境取2,有墙壁遮挡取3~4。

避坑指南:不要只用一次RSSI值做围栏判断。我建议连续采集3~5次,都满足条件再触发。这样可以避免瞬间波动导致的误触发。

4. 后台扫描:让App“睡着”也能工作

定位围栏最头疼的问题就是后台扫描。Android从8.0开始对后台限制越来越严。App一进后台,扫描就被系统掐了。

我的解决方案是使用Foreground Service。说白了,就是让App在前台跑一个服务,用户能看到一个常驻通知。

代码结构大概这样:

class BleScanService : Service() {
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // 创建通知
        val notification = createNotification()
        startForeground(NOTIFICATION_ID, notification)

        // 开始扫描
        startBleScan()
        return START_STICKY
    }

    private fun startBleScan() {
        val scanner = BluetoothLeScannerCompat.getScanner()
        val settings = ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
            .setLegacy(false)
            .build()
        val filters = listOf(
            ScanFilter.Builder()
                .setServiceUuid(ParcelUuid(UUID.fromString("你的UUID")))
                .build()
        )
        scanner.startScan(filters, settings, scanCallback)
    }
}

另外,别忘了在Android 12+上申请BLUETOOTH_SCANBLUETOOTH_CONNECT权限。我刚开始就漏了这个,扫描死活没结果。

关键点:后台扫描必须配合前台服务。否则系统会在几分钟内杀掉你的扫描进程。

5. 整体架构图

下面这张图展示了整个定位标签系统的核心流程。从Beacon广播到围栏触发,一目了然。

Beacon 设备 广播包 (UUID/Major/Minor/TxPower) 手机扫描 RSSI 采集与滤波 距离估算 围栏触发判断 触发动作(通知/事件) 前台服务保活 Beacon 手机端 围栏逻辑

6. 实战中的几个坑

最后,我把自己踩过的坑列出来,你遇到了直接绕开:

  • 扫描间隔:不要一直扫。扫3秒停5秒,省电又稳定。
  • UUID过滤:一定要加过滤。否则周围所有Beacon都会触发,手机直接卡死。
  • 多围栏管理:如果同时监控多个围栏,建议用Map管理,key用Minor值。
  • 系统兼容:华为、小米等厂商对后台限制更严。建议在主流机型上多测试。
我的经验:定位标签项目,80%的精力花在“稳定”上,20%花在“功能”上。先把RSSI滤波和后台保活做好,再谈其他。

好了,这节课的内容就是这些。Beacon广播、RSSI采集、围栏触发、后台扫描,这四个点串起来就是一个完整的定位标签系统。你动手试试,有问题随时交流。

公众号:蓝海资料掘金营,微信deep3321