DEV Community

Smrati
Smrati

Posted on

BLE Indoor Positioning: How RSSI Trilateration Works (With Math and Code)

RSSI and distance

Each BLE beacon sends out a signal which is picked up by fixed points and then the strength of the signal measured, which gives the Received Signal Strength Indicator (RSSI), measured in dBm. Higher strength means higher proximity. According to the Log-Distance Path Loss Equation:
d = 10 ^ ((TxPower − RSSI) / (10 × n))
Where:

d = Distance in metres
TxPower = RSSI at 1 m (usually calibrated, around −59 to −65 dBm)
RSSI = current RSSI in dBm
n = path loss exponent
(typically 2.0 for free space, 2.5 to 4.0 indoors)

function rssiToDistance(rssi, txPower = -59, n = 2.5) {
  if (rssi === 0) return -1
  const ratio = (txPower - rssi) / (10 * n)
  return Math.pow(10, ratio)
}

rssiToDistance(-59) // → ~1.0m
rssiToDistance(-70) // → ~3.5m
rssiToDistance(-85) // → ~14m
Enter fullscreen mode Exit fullscreen mode

⚠️ RSSI is noisy. A single reading varies ±10 dBm from interference. Never feed raw RSSI into trilateration — smooth first with a Kalman filter.

class KalmanFilter {
  constructor(R = 0.125, Q = 0.5) {
    this.R = R; this.Q = Q
    this.x = null; this.P = 1
  }
  filter(z) {
    if (this.x === null) { this.x = z; return z }
    const K = this.P / (this.P + this.R)
    this.x = this.x + K * (z - this.x)
    this.P = (1 - K) * this.P + this.Q
    return this.x
  }
}

// One filter per anchor per tag
const filter = new KalmanFilter()
const distance = rssiToDistance(filter.filter(rawRssi))
Enter fullscreen mode Exit fullscreen mode

Trilateration — the math

With distances from 3+ anchors at known positions, solve:
(x − x₁)² + (y − y₁)² = d₁²
(x − x₂)² + (y − y₂)² = d₂²
(x − x₃)² + (y − y₃)² = d₃²
Subtract equation 1 from equations 2 and 3 → eliminates x² and y² → two linear equations → solve for x and y.

Full implementation

const anchors = [
  { id: 'A1', x: 0,  y: 0 },
  { id: 'A2', x: 10, y: 0 },
  { id: 'A3', x: 5,  y: 8 },
  { id: 'A4', x: 0,  y: 8 },
]

function trilaterate(anchors, distances) {
  const ref = anchors[0]
  const A = [], b = []

  for (let i = 1; i < anchors.length; i++) {
    const a = anchors[i]
    A.push([2*(a.x-ref.x), 2*(a.y-ref.y)])
    b.push(
      distances[i]**2 - distances[0]**2
      - a.x**2 + ref.x**2
      - a.y**2 + ref.y**2
    )
  }
  const [x, y] = leastSquares(A, b)
  return { x: Math.round(x*100)/100, y: Math.round(y*100)/100 }
}

function leastSquares(A, b) {
  let AtA = [[0,0],[0,0]], Atb = [0,0]
  for (let i = 0; i < A.length; i++) {
    const [a0,a1] = A[i]
    AtA[0][0]+=a0*a0; AtA[0][1]+=a0*a1
    AtA[1][0]+=a1*a0; AtA[1][1]+=a1*a1
    Atb[0]+=a0*b[i];  Atb[1]+=a1*b[i]
  }
  const det = AtA[0][0]*AtA[1][1] - AtA[0][1]*AtA[1][0]
  return [
    (AtA[1][1]*Atb[0] - AtA[0][1]*Atb[1]) / det,
    (AtA[0][0]*Atb[1] - AtA[1][0]*Atb[0]) / det
  ]
}
Enter fullscreen mode Exit fullscreen mode

Real World Accuracy

Environment Path Loss n Accuracy Notes
Open Office 2.0 - 2.5 1 – 2m Best conditions
Warehouse 2.5 – 3.0 2 – 4m Metal shelving multipath
Hospital 3.0 – 3.5 2 – 5m Lots of dense walls, equipment
Manufacturing 3.0 – 4.0 3 – 6m Heavier machinery, RF-dense

Tip for Calibration: calibrate txPower individually at each anchor point in your environment. Stand 1m away, take 100 RSSI samples. Calibration increases accuracy by 30-40% over manufacture default values.

What to Build on Top

Place the trilaterated coordinate system onto a SVG floor plan – correlate the meters from the real world to pixels using scale factor. Asset pin locations are updated based on incoming RSSI coordinates received via Server-Sent Events or WebSocket.

AssetTrackPro's BLE indoor positioning technology is based on calibrated RSSI trilateration + Kalman Filtering, delivering accurate positions

Explore AssetTrackPro ↗

Top comments (0)