Estimating Distance from BLE RSSI with a Path-Loss Model
Ranging is the single conversion that every BLE trilateration solve depends on, and it is where most positioning error is born; this page zooms in on turning one beacon’s RSSI into one distance, the input the BLE beacon positioning solver intersects to find a fix.
What the Log-Distance Path-Loss Model Means
Radio signal strength decays predictably with distance, and the log-distance path-loss model captures that decay in a form you can invert. In its ranging form it says the received strength at distance d equals the strength at a one-metre reference minus a logarithmic term:
rssi(d) = tx_power - 10 * n * log10(d)
Two parameters do all the work. tx_power — often called measured power or the calibrated RSSI at one metre — is the anchor of the whole curve: it is the RSSI, in dBm, that this specific beacon in this specific enclosure produces at exactly one metre. n, the path-loss exponent, is the decay rate: n = 2 is free-space, open halls sit near 1.8-2.2, and cluttered partitioned offices climb to 3.0-3.5 as walls and furniture absorb signal.
Solving for d gives the ranging equation you actually call:
d = 10 ** ((tx_power - rssi) / (10 * n))
Two properties of this equation drive every practical decision. It is exponential in RSSI, so error grows with distance — a fixed ±6 dB of noise is a small range error up close and a large one far away. And it is exquisitely sensitive to tx_power: a beacon whose true one-metre power is 4 dB below the value you assumed reports every distance biased long, so calibration is not optional polish, it is the difference between a two-metre fix and a systematic five-metre error.
Minimal Working Example
The example below smooths a stream of raw RSSI samples with an exponential moving average before ranging — because a single BLE packet can be 8-10 dB off — then inverts the model. Smoothing the RSSI, not the resulting distance, is what keeps the exponential from amplifying single-packet spikes.
import logging
import math
from collections import deque
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
class RssiRanger:
"""Smooth BLE RSSI with an EMA, then invert the log-distance path-loss model."""
def __init__(self, tx_power: float, n: float = 2.2, alpha: float = 0.3) -> None:
if not 0.0 < alpha <= 1.0:
raise ValueError(f"alpha must be in (0, 1], got {alpha}")
self.tx_power = tx_power # calibrated RSSI at 1 m, dBm
self.n = n # path-loss exponent for this space
self.alpha = alpha # EMA weight on the newest sample
self._ema: float | None = None
self._window: deque[float] = deque(maxlen=8)
def update(self, rssi: float) -> float:
"""Feed one raw RSSI sample (dBm); return the smoothed range in metres."""
self._window.append(rssi)
self._ema = rssi if self._ema is None else self.alpha * rssi + (1 - self.alpha) * self._ema
try:
distance = 10.0 ** ((self.tx_power - self._ema) / (10.0 * self.n))
except OverflowError:
logger.warning("range overflow at rssi=%.1f; clamping to 40 m", self._ema)
return 40.0
distance = min(max(distance, 0.3), 40.0)
logger.info("rssi=%.1f ema=%.1f -> %.2f m", rssi, self._ema, distance)
return distance
ranger = RssiRanger(tx_power=-59.0, n=2.2)
for sample in (-71.0, -68.0, -95.0, -70.0, -69.0): # note the -95 dBm spike
ranger.update(sample)
The -95 spike barely moves the smoothed range because the EMA discounts it, whereas ranging each raw sample directly would have thrown a phantom “beacon is 15 m away” reading straight into the solver.
Path-Loss Parameter Reference
| Parameter | Symbol | Typical range | Notes |
|---|---|---|---|
| Calibrated transmit power | tx_power |
−45 to −65 dBm | RSSI measured at exactly 1 m, per beacon; anchors the whole curve |
| Path-loss exponent | n |
1.6 (open) – 3.5 (partitioned) | Environment decay rate; one value per floor-type usually suffices |
| Raw RSSI | rssi |
−30 to −99 dBm | Instantaneous per-packet reading; noisy by ±8-10 dB |
| EMA weight | alpha |
0.2 – 0.4 | Lower is smoother but laggier; trade responsiveness against noise |
| Smoothing window | — | 5 – 10 samples | Enough to average a burst without lagging a walking user |
| Range clamp | — | 0.3 – 40 m | Reject reflections claiming sub-decimetre or cross-building distance |
Common Errors & Fixes
Every distance is biased long (or short) by a constant factor. The tx_power you passed does not match the beacon’s true one-metre power — usually a datasheet nominal used instead of an in-situ measurement, or a beacon whose battery has sagged. Calibrate by placing the receiver exactly one metre away, averaging a few hundred RSSI samples, and using that mean as tx_power:
def calibrate_tx_power(samples_at_1m: list[float]) -> float:
"""Mean RSSI at 1 m becomes this beacon's tx_power. Log the spread as a health check."""
if len(samples_at_1m) < 30:
raise ValueError("collect >= 30 samples at 1 m for a stable tx_power")
mean = sum(samples_at_1m) / len(samples_at_1m)
spread = max(samples_at_1m) - min(samples_at_1m)
logger.info("calibrated tx_power=%.1f dBm (spread %.1f dB)", mean, spread)
return mean
Ranges are plausible in the atrium but far too short in offices. You used one n everywhere. An open hall near n = 1.9 and a partitioned floor near n = 3.2 decay at very different rates; a single exponent over-ranges one and under-ranges the other. Measure n per space type from two or three known distances and pick it by floor, not globally.
The fix jitters even though the beacons are stationary. You ranged a single raw sample instead of a smoothed one. One BLE packet carries ±8-10 dB of noise, which the exponential turns into metres of range wobble. Maintain an EMA or running mean per beacon and range the smoothed value, as in the working example above.
Integration Point
This ranging step is the front end of the whole positioning chain. Each smoothed distance it emits becomes one circle in the least-squares trilateration solved in BLE beacon positioning, so a calibration error here propagates straight into every fix downstream. The quality of those fixes in turn depends on where the beacons sit — good ranging cannot rescue collinear anchors, which is the subject of the companion guide on optimal BLE beacon placement for floor coverage. Finally, because even smoothed ranges carry residual noise, the resulting fixes are not shown raw: they feed the sensor fusion and particle filter stage that combines them with motion into the stable track a map renders.
Frequently Asked Questions
Should I smooth the RSSI or the computed distance?
Smooth the RSSI, before ranging. The path-loss inversion is exponential, so noise on the RSSI is amplified non-linearly into the distance; averaging after the exponential lets a single spurious spike briefly dominate the mean before it decays. Keep a per-beacon EMA or short running mean of the raw dBm values and pass the smoothed RSSI into the model. This also keeps the smoothing parameters in the units you actually measured.
How do I choose the path-loss exponent for my building?
Measure it, do not guess. Record averaged RSSI at two or three known distances in a representative space, then fit n from rssi = tx_power - 10 * n * log10(d) by solving for the slope. Open atria land near 1.8-2.2 and partitioned offices near 3.0-3.5. One exponent per floor-type is enough; a single global value will systematically over- or under-range whichever environment it does not match.
Why is my distance estimate worse at longer range?
Because the model is exponential in RSSI, a fixed amount of signal noise maps to a growing distance error as range increases — the same ±6 dB is a few centimetres at one metre and several metres at fifteen. This is inherent to RSSI ranging, not a bug. Mitigate it by weighting nearer beacons more heavily in the trilateration solve and by densifying beacons so the receiver usually has a close, well-conditioned anchor.
Related
- BLE Beacon Positioning & Trilateration — how these ranges become circles a least-squares solver intersects.
- Optimal BLE Beacon Placement for Floor Coverage — the placement geometry that determines how far each range must reach.
- Sensor Fusion & Particle Filters — the stage that absorbs residual ranging noise into a stable track.
This page is a companion to BLE Beacon Positioning & Trilateration, part of the Indoor Positioning & Fingerprinting reference.