蓝牙定位标签实战:Beacon广播、RSSI采集、围栏触发、后台扫描
各位同学,今天我们来聊一个非常实战的话题——蓝牙定位标签。说白了,就是让手机知道“你离某个蓝牙设备有多近”,然后根据距离做点事情。
我在做第一个BLE定位项目时,踩了不少坑。比如RSSI值忽高忽低,围栏动不动就误触发。嗯,今天我把这些经验都揉进这节课里,你学完就能直接上手。
1. Beacon广播:定位的“灯塔”
Beacon本质上就是一个不停广播的BLE设备。它不连接,只发数据包。手机端收到广播包,就能算出距离。
一个标准的iBeacon广播包,核心字段就这几个:
| 字段 | 长度 | 说明 |
|---|---|---|
| UUID | 16字节 | 标识组织/应用 |
| Major | 2字节 | 标识区域(如楼层) |
| Minor | 2字节 | 标识具体设备(如工位) |
| Tx Power | 1字节 | 1米处的参考RSSI值 |
我个人习惯把UUID当作“项目ID”,Major当作“楼层”,Minor当作“具体点位”。这样后期维护起来特别清晰。
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。
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_SCAN和BLUETOOTH_CONNECT权限。我刚开始就漏了这个,扫描死活没结果。
5. 整体架构图
下面这张图展示了整个定位标签系统的核心流程。从Beacon广播到围栏触发,一目了然。
6. 实战中的几个坑
最后,我把自己踩过的坑列出来,你遇到了直接绕开:
- 扫描间隔:不要一直扫。扫3秒停5秒,省电又稳定。
- UUID过滤:一定要加过滤。否则周围所有Beacon都会触发,手机直接卡死。
- 多围栏管理:如果同时监控多个围栏,建议用Map管理,key用Minor值。
- 系统兼容:华为、小米等厂商对后台限制更严。建议在主流机型上多测试。
好了,这节课的内容就是这些。Beacon广播、RSSI采集、围栏触发、后台扫描,这四个点串起来就是一个完整的定位标签系统。你动手试试,有问题随时交流。