你有没有遇到过这种情况——
设备在室外运行好好的,GPS坐标稳、精度两三米。一进仓库,getLastKnownLocation() 返回null,或者给你一个五分钟前的老坐标。调度系统直接失去对设备的感知,运营那边开始催你。
这不是代码写错了,是物理限制。GPS是L波段信号,穿透力差,混凝土和金属屋顶是天然屏蔽。室内本来就不是GPS的主场。
这篇想聊三件事:融合定位在技术层面怎么工作的、工程上有哪些坑、怎么用API快速接进自己的系统。
融合定位靠什么信号?
不是魔法,就三类射频信号。
基站
每个移动基站有唯一标识:MCC + MNC + LAC/TAC + Cell ID。终端在任意时刻都在某个基站覆盖范围内,查数据库匹配坐标,得到粗略位置。
精度100米到几公里,取决于基站密度。城市中心密,能做到100米级;农村基站稀,误差可能好几公里。
多基站时可以根据RSSI信号强度做三角加权,精度能拉到50~200米。
WiFi
这是室内精度最高的方案。WiFi AP的MAC地址全局唯一,商业数据库记录了海量AP的地理位置(通过众包积累)。扫描周边AP列表,用MAC地址查位置,再按RSSI加权,精度通常15~50米。
大型商场、写字楼、地铁站的AP密度高,精度可以做到15米以内。你在商场里没开GPS手机还知道你在几楼,靠的就是这个。
数据融合层
实际工程里不会只用一种信号——WiFi AP可能关机、基站受地形影响。成熟的融合定位服务同时接多种信号输入,在服务端做加权融合,输出一个置信度更高的坐标。
工程上真实会踩的坑
Android WiFi扫描越来越受限
Android 9(API 28)之后,WifiManager.startScan() 被节流:前台App最多每2分钟扫4次,后台更少。Android 10开始部分厂商ROM直接关掉了后台WiFi扫描。
如果你做的是消费端App,这一块要提前确认。老的WifiScan方案在新Android上基本不可靠了。IoT设备跑定制系统或直接读硬件的影响相对小,但也要确认。
基站参数采集别漏字段
Android上用TelephonyManager.getAllCellInfo()拿基站数据,不同网络制式(GSM、LTE、NR)字段名不一样,要做类型判断:
val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
// 需要 READ_PHONE_STATE 权限
tm.allCellInfo?.forEach { info ->
when (info) {
is CellInfoLte -> {
val id = info.cellIdentity
val sig = info.cellSignalStrength
// id.ci => Cell ID
// id.tac => TAC(LTE区域码)
// id.mccString => MCC
// id.mncString => MNC
// sig.dbm => 信号强度 dBm
// sig.asuLevel => ASU
}
is CellInfoGsm -> {
// id.cid => Cell ID
// id.lac => LAC(GSM区域码)
}
is CellInfoNr -> { /* 5G,字段又不一样 */ }
}
}
坐标系一定要确认清楚
这个坑很隐蔽:融合定位返回的坐标,不同服务商用的坐标系不同。如果API给你WGS84,你直接拿去高德地图展示,会有50~300米的系统性偏移——不是精度差,是坐标系没对齐。
接入前先确认返回的是什么坐标系,以及你的地图渲染层用的是什么。
接一个真实API:完整流程
说完原理,看看实际接入。用迈云位置服务 LTS 的融合定位接口演示,POST /api/service/location,Bearer Token认证,支持返回GCJ02。
请求结构
传WiFi列表(精度更高,优先用):
javascript
fetch('https://lts.maiyun.net/api/service/location', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 3, // 3 = GCJ02
time: Date.now(), // 毫秒时间戳
asset: 'device-warehouse-01', // 设备唯一ID,用于轨迹
wifis: [
{ mac: '9E:2B:A6:86:2A:0E', signal: -74 },
{ mac: '54:52:84:86:03:A8', signal: -79 },
{ mac: 'A8:6D:AA:12:34:56', signal: -85 },
],
}),
})
.then(r => r.json())
.then(data => {
if (data.result === 1) {
console.log(data.point.lat, data.point.lng)
console.log(data.address.name) // 完整地址,到街道级
}
})
降级用基站(WiFi扫不到时兜底):
javascript
body: JSON.stringify({
from: 3,
time: Date.now(),
asset: 'device-warehouse-01',
cellulars: [
{
cell: 12345678, // Cell ID
primary: true, // 是否主基站
dbm: -85, // 信号强度
country: 460, // MCC:中国
network: 0, // MNC:中国移动
area: 4321 // TAC(LTE)
}
]
})
返回结构
json
{
"result": 1,
"name": "某仓储广场B区",
"point": { "lng": 117.334369, "lat": 39.116094 },
"address": {
"name": "天津市东丽区万新街道平盈路8号",
"context": {
"province": { "name": "天津市", "code": "120000" },
"city": { "name": "天津市", "code": "120100" },
"district": { "name": "东丽区", "code": "120110" },
"township": { "name": "万新街道", "code": "120110006" }
}
}
}
address.context 直接到街道级,物流末端、区域分析场景不需要再额外发一次逆地址解析请求,省一次调用。
降级策略:-3 这个错误码别忽略
接口有个 -3,含义是"信号数据不足,无法完成定位"。极端弱信号环境(全屏蔽机房、电梯深处)会出现。
推荐的降级顺序:
GPS 可用
└→ 直接用 GPS
GPS 弱/不可用
└→ 融合定位(WiFi 优先,基站兜底)
融合定位返回 -3
└→ 使用上一次成功坐标 + 标记 stale: true
全部失败
└→ 上报 location_status: "unavailable",不上传坐标
有一个常见的低级错误:在 -3 时把 point 设成 {lat: 0, lng: 0} 存进数据库。等轨迹回放的时候,设备轨迹线会突然飞到几内亚湾再飞回来。
一个完整的 Android 采集上报封装
把上面的串起来:
class FusedLocationReporter(
private val context: Context,
private val apiKey: String,
private val deviceId: String
) {
private val httpClient = OkHttpClient()
private fun collectWifis(): List<Map<String, Any>> {
val wm = context.applicationContext
.getSystemService(Context.WIFI_SERVICE) as WifiManager
return wm.scanResults.map {
mapOf("mac" to it.BSSID, "signal" to it.level)
}
}
private fun collectCellulars(): List<Map<String, Any>> {
val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
return tm.allCellInfo
?.filterIsInstance<CellInfoLte>()
?.map { info ->
mapOf(
"cell" to info.cellIdentity.ci,
"primary" to info.isRegistered,
"dbm" to info.cellSignalStrength.dbm,
"country" to (info.cellIdentity.mccString?.toIntOrNull() ?: 0),
"network" to (info.cellIdentity.mncString?.toIntOrNull() ?: 0),
"area" to info.cellIdentity.tac
)
} ?: emptyList()
}
suspend fun report(): Result<LocationData> = withContext(Dispatchers.IO) {
runCatching {
val wifis = collectWifis()
val cellulars = collectCellulars()
// 无信号数据直接跳出
if (wifis.isEmpty() && cellulars.isEmpty()) {
throw IllegalStateException("no signal data")
}
val payload = buildMap {
put("from", 3)
put("time", System.currentTimeMillis())
put("asset", deviceId)
// WiFi 优先
if (wifis.isNotEmpty()) put("wifis", wifis)
else put("cellulars", cellulars)
}
val request = Request.Builder()
.url("https://lts.maiyun.net/api/service/location")
.addHeader("Authorization", "Bearer $apiKey")
.post(
Gson().toJson(payload)
.toRequestBody("application/json".toMediaType())
)
.build()
val raw = httpClient.newCall(request).execute()
.body?.string() ?: throw IOException("empty response")
Gson().fromJson(raw, LocationData::class.java)
.also { if (it.result != 1) throw RuntimeException("locate failed: ${it.result}") }
}
}
}
融合定位不是银弹,精度上限在那里,WiFi密集区15米、基站兜底100米+。但在GPS完全不可用的场景下,有一个粗略的位置感知,远比完全失联要好得多。
自建WiFi/基站数据库基本不现实(迈云LTS那边有20亿+ WiFi AP积累量),用成品服务接入是唯一合理的路。lts.maiyun.net 文档和控制台都可以直接体验,注册后能看到调用量和错误率统计。
有问题或者有不同实现方式的,评论区聊。