[特殊字符] TypeScript 版本 Beacon 信号滤波与室内定位引擎
在室内导航或定位系统中,iBeacon 蓝牙信号 是常见的基础数据源。
然而 RSSI(信号强度)波动剧烈,容易导致定位点“跳动”。
这篇文章将带你一步步实现一个可运行在 WebView / Vue 应用 中的
高精度 信号平滑 + 速度约束 + 多边定位(multilateration) 引擎。
🧩 一、核心目标
-
对多台 Beacon 的 RSSI 信号进行平滑滤波
-
计算设备与 Beacon 的距离(基于路径损耗模型)
-
用多边测量算法(Levenberg–Marquardt)求解设备坐标
-
添加速度限制与动态信号阈值,保证稳定性
-
自动过滤异常或过期信号
最终输出设备的平滑坐标 (x, y) 与信号状态(弱、中、强)。
⚙️ 二、BeaconSignalProcessor 核心结构
我们定义一组类型,让信号、位置与配置都强类型化:
interface IBeacon {
uuid: string
major: number
minor: number
rssi: number
accuracy: number
}
export type ISignalData = IBeacon & {
name: string
x: number
y: number
z: number
smoothedRSSI: number
distance: number
timestamp: number
}
interface IPosition {
x: number
y: number
z?: number
signalStatus?: 'weak' | 'medium' | 'strong'
}
interface Config {
distanceExceedsRadius?: number
maxPacketAge?: number
minimumBeaconCount?: number
maximumBeaconCount?: number
minimumRssiThreshold?: number
maximumMovementSpeed?: number
txPowerAt1m?: number
beaconHistoryFilterlength?: number
ceilingHeight?: number
}
🧠 三、距离计算:路径损耗模型
Beacon 的距离估算通常使用 对数路径损耗模型:
private calculateDistance(rssi: number): number {
const txPower = this.config.txPowerAt1m
const n = 4.4 // 室内路径损耗因子
const d = 10 ** ((txPower - rssi) / (10 * n))
// 若考虑Z轴高度投影
if (d > this.config.ceilingHeight * 2)
return Math.sqrt(d ** 2 - this.config.ceilingHeight ** 2)
return d
}
该模型能根据信号衰减估算与发射源的距离(单位米)。
🧮 四、信号平滑:动态窗口平均 + 众数容差滤波
由于 RSSI 波动不可避免,我们设计了两层平滑策略:
-
滑动窗口平均:对每个 Beacon 维护最近一段时间的历史记录;
-
众数容差平均:根据信号分布动态选择容差,滤除异常峰值。
public getMajorityAverageRSSI(
list: number[],
weakThreshold = -70,
baseTolerance = 3,
weakTolerance = 5,
): number {
if (!list.length) return 0
const mean = list.reduce((a, b) => a + b, 0) / list.length
const stdDev = Math.sqrt(list.reduce((a, b) => a + (b - mean) ** 2, 0) / list.length)
const dynamicTolerance = mean < weakThreshold
? Math.max(baseTolerance, Math.ceil(stdDev), weakTolerance)
: Math.max(baseTolerance, Math.ceil(stdDev))
const freq = new Map<number, number>()
list.forEach(rssi => freq.set(rssi, (freq.get(rssi) || 0) + 1))
const mode = [...freq.entries()].reduce((a, b) => (b[1] > a[1] ? b : a))[0]
return Math.ceil(mode + dynamicTolerance)
}
这种方法在信号较弱或波动较大时,会自动放宽容差范围。
📍 五、多边测量(Multilateration)求位置
利用 3 个及以上 beacon 的位置与距离,我们可以通过 最小二乘拟合 估算设备位置。
这里采用简化版 Levenberg–Marquardt 算法:
private multilateration(beacons: ISignalData[]): IPosition | null {
if (beacons.length < this.config.minimumBeaconCount) return null
const positions = beacons.map(b => [b.x, b.y])
const distances = beacons.map(b => b.distance)
// 初始猜测:加权平均
const weights = beacons.reduce((s, b) => s + 1 / b.distance, 0)
let x = beacons.reduce((s, b) => s + b.x / b.distance, 0) / weights
let y = beacons.reduce((s, b) => s + b.y / b.distance, 0) / weights
const lambda = 0.01
for (let i = 0; i < 15; i++) {
const J: number[][] = []
const r: number[] = []
beacons.forEach((b, i) => {
const dx = x - positions[i][0]
const dy = y - positions[i][1]
const d = Math.sqrt(dx * dx + dy * dy)
r.push(d - distances[i])
J.push([dx / d, dy / d])
})
// 正规方程求解 Δx, Δy
const JTJ = [
[J.reduce((s, j) => s + j[0] * j[0], lambda), J.reduce((s, j) => s + j[0] * j[1], 0)],
[J.reduce((s, j) => s + j[1] * j[0], 0), J.reduce((s, j) => s + j[1] * j[1], lambda)],
]
const JTr = [J.reduce((s, j, i) => s + j[0] * r[i], 0), J.reduce((s, j, i) => s + j[1] * r[i], 0)]
const det = JTJ[0][0] * JTJ[1][1] - JTJ[0][1] * JTJ[1][0]
if (Math.abs(det) < 1e-6) break
const dx = (-JTr[0] * JTJ[1][1] + JTr[1] * JTJ[0][1]) / det
const dy = (-JTr[1] * JTJ[0][0] + JTr[0] * JTJ[1][0]) / det
x += dx; y += dy
if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01) break
}
return { x, y }
}
🚦 六、速度限制滤波:防止“瞬移”
当信号异常跳动导致位置突变时,我们根据时间间隔限制移动速度:
private speedFilter(current: IPosition, prev: IPosition, dt: number): IPosition {
const dx = current.x - prev.x
const dy = current.y - prev.y
const distance = Math.sqrt(dx * dx + dy * dy)
const maxDist = this.config.maximumMovementSpeed * (dt / 1000)
if (distance <= maxDist) return current
const scale = maxDist / distance
return { x: prev.x + dx * scale, y: prev.y + dy * scale }
}
📊 七、主流程:信号 → 滤波 → 距离 → 定位
public getCurrentPosition(rawBeacons: ISignalData[]): IPosition | null {
const active = this.filterBeacons(rawBeacons)
if (active.length < this.config.minimumBeaconCount && !this.positioningCount)
return null
const processed = active.map(beacon => {
const history = this.rssiHistories.get(+beacon.minor) || []
history.push({ name: beacon.name, rssi: beacon.rssi, timestamp: Date.now() })
this.rssiHistories.set(+beacon.minor, history)
const avgRssi = this.getSmoothedAverage(history, h => h.rssi)
const distance = this.calculateDistance(avgRssi)
return { ...beacon, smoothedRSSI: avgRssi, distance }
})
const beacons = processed.sort((a, b) => b.smoothedRSSI - a.smoothedRSSI)
.slice(0, this.config.maximumBeaconCount)
const position = this.multilateration(beacons)
if (!position) return null
const final = this.speedFilter(position, this.lastPosition || position, Date.now() - this.lastUpdateTime)
this.lastPosition = final
this.positioningCount++
return final
}
📡 八、完整代码
// utils/BeaconSignalProcessor.ts
// ----------------------------------------------------
// 用于高精度 iBeacon 信号滤波与室内定位的核心引擎
// ----------------------------------------------------
export interface IBeacon {
uuid: string
major: number
minor: number
rssi: number
accuracy: number
}
export type ISignalData = IBeacon & {
name: string
x: number
y: number
z: number
smoothedRSSI: number
distance: number
timestamp: number
}
export interface IPosition {
x: number
y: number
z?: number
signalStatus?: 'weak' | 'medium' | 'strong'
}
export interface Config {
distanceExceedsRadius?: number
maxPacketAge?: number
minimumBeaconCount?: number
maximumBeaconCount?: number
minimumRssiThreshold?: number
maximumMovementSpeed?: number
txPowerAt1m?: number
beaconHistoryFilterlength?: number
ceilingHeight?: number
}
export default class BeaconSignalProcessor {
private config: Required<Config>
private rssiHistories = new Map<number, { name: string; rssi: number; timestamp: number }[]>()
private lastPosition: IPosition | null = null
private lastUpdateTime = Date.now()
private positioningCount = 0
constructor(config?: Config) {
this.config = {
distanceExceedsRadius: config?.distanceExceedsRadius ?? 10,
maxPacketAge: config?.maxPacketAge ?? 3000,
minimumBeaconCount: config?.minimumBeaconCount ?? 3,
maximumBeaconCount: config?.maximumBeaconCount ?? 6,
minimumRssiThreshold: config?.minimumRssiThreshold ?? -90,
maximumMovementSpeed: config?.maximumMovementSpeed ?? 1.5,
txPowerAt1m: config?.txPowerAt1m ?? -59,
beaconHistoryFilterlength: config?.beaconHistoryFilterlength ?? 8,
ceilingHeight: config?.ceilingHeight ?? 3,
}
}
// -------- 距离计算 --------
private calculateDistance(rssi: number): number {
const txPower = this.config.txPowerAt1m
const n = 4.4
const d = 10 ** ((txPower - rssi) / (10 * n))
if (d > this.config.ceilingHeight * 2)
return Math.sqrt(d ** 2 - this.config.ceilingHeight ** 2)
return d
}
// -------- 众数容差平均 --------
public getMajorityAverageRSSI(
list: number[],
weakThreshold = -70,
baseTolerance = 3,
weakTolerance = 5,
): number {
if (!list.length) return 0
const mean = list.reduce((a, b) => a + b, 0) / list.length
const stdDev = Math.sqrt(list.reduce((a, b) => a + (b - mean) ** 2, 0) / list.length)
const dynamicTolerance = mean < weakThreshold
? Math.max(baseTolerance, Math.ceil(stdDev), weakTolerance)
: Math.max(baseTolerance, Math.ceil(stdDev))
const freq = new Map<number, number>()
list.forEach(rssi => freq.set(rssi, (freq.get(rssi) || 0) + 1))
const mode = [...freq.entries()].reduce((a, b) => (b[1] > a[1] ? b : a))[0]
return Math.ceil(mode + dynamicTolerance)
}
private getSmoothedAverage<T>(arr: T[], selector: (v: T) => number): number {
const values = arr.slice(-this.config.beaconHistoryFilterlength).map(selector)
return this.getMajorityAverageRSSI(values)
}
// -------- 过滤过期或弱信号 --------
private filterBeacons(beacons: ISignalData[]): ISignalData[] {
const now = Date.now()
return beacons.filter(b =>
b.rssi >= this.config.minimumRssiThreshold &&
now - b.timestamp <= this.config.maxPacketAge,
)
}
// -------- 多边测量算法 --------
private multilateration(beacons: ISignalData[]): IPosition | null {
if (beacons.length < this.config.minimumBeaconCount) return null
const positions = beacons.map(b => [b.x, b.y])
const distances = beacons.map(b => b.distance)
const weights = beacons.reduce((s, b) => s + 1 / b.distance, 0)
let x = beacons.reduce((s, b) => s + b.x / b.distance, 0) / weights
let y = beacons.reduce((s, b) => s + b.y / b.distance, 0) / weights
const lambda = 0.01
for (let i = 0; i < 15; i++) {
const J: number[][] = []
const r: number[] = []
beacons.forEach((b, i) => {
const dx = x - positions[i][0]
const dy = y - positions[i][1]
const d = Math.sqrt(dx * dx + dy * dy)
r.push(d - distances[i])
J.push([dx / d, dy / d])
})
const JTJ = [
[J.reduce((s, j) => s + j[0] * j[0], lambda), J.reduce((s, j) => s + j[0] * j[1], 0)],
[J.reduce((s, j) => s + j[1] * j[0], 0), J.reduce((s, j) => s + j[1] * j[1], lambda)],
]
const JTr = [
J.reduce((s, j, i) => s + j[0] * r[i], 0),
J.reduce((s, j, i) => s + j[1] * r[i], 0),
]
const det = JTJ[0][0] * JTJ[1][1] - JTJ[0][1] * JTJ[1][0]
if (Math.abs(det) < 1e-6) break
const dx = (-JTr[0] * JTJ[1][1] + JTr[1] * JTJ[0][1]) / det
const dy = (-JTr[1] * JTJ[0][0] + JTr[0] * JTJ[1][0]) / det
x += dx
y += dy
if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01) break
}
return { x, y }
}
// -------- 速度限制滤波 --------
private speedFilter(current: IPosition, prev: IPosition, dt: number): IPosition {
const dx = current.x - prev.x
const dy = current.y - prev.y
const distance = Math.sqrt(dx * dx + dy * dy)
const maxDist = this.config.maximumMovementSpeed * (dt / 1000)
if (distance <= maxDist) return current
const scale = maxDist / distance
return { x: prev.x + dx * scale, y: prev.y + dy * scale }
}
// -------- 主流程:输入信号 → 输出位置 --------
public getCurrentPosition(rawBeacons: ISignalData[]): IPosition | null {
const active = this.filterBeacons(rawBeacons)
if (active.length < this.config.minimumBeaconCount && !this.positioningCount)
return null
const processed = active.map(beacon => {
const history = this.rssiHistories.get(+beacon.minor) || []
history.push({ name: beacon.name, rssi: beacon.rssi, timestamp: Date.now() })
this.rssiHistories.set(+beacon.minor, history)
const avgRssi = this.getSmoothedAverage(history, h => h.rssi)
const distance = this.calculateDistance(avgRssi)
return { ...beacon, smoothedRSSI: avgRssi, distance }
})
const beacons = processed.sort((a, b) => b.smoothedRSSI - a.smoothedRSSI)
.slice(0, this.config.maximumBeaconCount)
const position = this.multilateration(beacons)
if (!position) return null
const now = Date.now()
const dt = now - this.lastUpdateTime
this.lastUpdateTime = now
const final = this.lastPosition
? this.speedFilter(position, this.lastPosition, dt)
: position
this.lastPosition = final
this.positioningCount++
return final
}
}
// -------- 使用示例 --------
// import BeaconSignalProcessor from '@/utils/BeaconSignalProcessor'
// const processor = new BeaconSignalProcessor({ maximumMovementSpeed: 2 })
// const pos = processor.getCurrentPosition(beaconArray)
// console.log(pos)
🍷 九、使用示例
import BeaconSignalProcessor from '@/utils/BeaconSignalProcessor'
const processor = new BeaconSignalProcessor({ maximumMovementSpeed: 2 })
function handleBeacons(beaconData: ISignalData[]) {
const position = processor.getCurrentPosition(beaconData)
if (position) {
console.log(`当前位置: (${position.x.toFixed(2)}, ${position.y.toFixed(2)})`)
}
}
🧭 十、算法稳定性优化小结
| 策略 | 说明 |
|---|---|
| RSSI 历史窗口 | 平滑短期波动 |
| 动态容差平均 | 弱信号自动放宽阈值 |
| 异常过滤 | 丢弃距离变化过大的信号 |
| 速度约束 | 防止位置突变 |
| 动态信号等级 | 按平均 RSSI 分级展示信号状态 |
🎯 十一、总结
通过这套模块化的 Beacon 信号处理引擎,我们实现了:
-
🔹 从嘈杂信号中提取稳定定位信息
-
🔹 动态适配 iOS / Android 不同设备特性
-
🔹 精度、鲁棒性、可维护性三者平衡
-
🔹 TypeScript 类型驱动开发,提高安全性
最终效果:定位稳定、波动小、响应迅速。
📘 小结:
Beacon 定位的精度取决于信号处理策略的“韧性”。
算法可以精简,但滤波与约束必须科学。
通过 TypeScript 构建这样的定位核心,让室内导航项目更具可控性与可扩展性。
🧠 延伸阅读
如果你想进一步优化,可以尝试:
-
加入卡尔曼滤波(Kalman Filter)实现动态状态估计
-
用 WebAssembly 加速矩阵计算部分
-
支持三维定位
(x, y, z)
📍让信号噪声不再是敌人,而是可被驯化的输入。
从 RSSI 到定位,每一次平滑,都是算法的“温柔一刀”。
— BeaconSignalProcessor 作者笔记
更多推荐


所有评论(0)