在室内导航或定位系统中,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 波动不可避免,我们设计了两层平滑策略:

  1. 滑动窗口平均:对每个 Beacon 维护最近一段时间的历史记录;

  2. 众数容差平均:根据信号分布动态选择容差,滤除异常峰值。

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 作者笔记

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐