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)

The log-distance path-loss curve at three environment exponents Three curves of expected RSSI against true distance, all anchored at the same one-metre reference power of minus 59 dBm. With a path-loss exponent of 1.8, typical of an open hall, RSSI falls to about minus 77 dBm at 30 metres. With 2.4, typical of an open-plan office, it reaches about minus 94. With 3.6, typical of a partitioned floor, it passes minus 100 before 15 metres. At ten metres the three models disagree by roughly 18 dB, which is the whole reason the exponent must be calibrated per floor. One reference power, three environments, 18 dB of disagreement -100 -80 -60 10 20 30 true distance from the beacon (m) expected RSSI (dBm) n = 1.8 (open hall) n = 2.4 (open-plan office) n = 3.6 (partitioned floor) at 10 m the three models differ by 18 dB

The exponent is not a constant. Borrowing a textbook n = 2.0 for a partitioned floor puts every fix systematically too far from its beacon — a bias, not noise, so no amount of averaging removes it.

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
The five parameters of a calibrated path-loss ranging model A reference table of five parameters. The reference power at one metre, tx_power, is typically around minus 59 dBm and must be measured rather than taken from a datasheet. The path-loss exponent n ranges from 1.8 to 3.6 and is fitted on surveyed points, per floor. The shadowing standard deviation sigma is 3 to 6 decibels and is the residual spread left after the fit. The ranging floor is about minus 95 dBm, below which a reading is discarded rather than converted. The smoothing window is 5 to 10 samples and should use a median rather than a mean so that outliers are rejected. Two parameters to fit, three measurements to take Parameter Symbol Typical How you get it Reference power at 1 m tx_power -59 dBm measure it; do not trust the datasheet Path-loss exponent n 1.8 - 3.6 fit on surveyed points, per floor Shadowing std. dev. sigma 3 - 6 dB residual spread after the fit Ranging floor rssi_min -95 dBm below this, discard the reading Smoothing window N 5 - 10 samples median, not mean — reject outliers Only two of these are free parameters; the rest are measurements you are obliged to take.

Datasheet tx_power is the most common calibration bug. Enclosure, mounting surface and antenna orientation move it by 5-8 dB, which the model then converts into a systematic distance offset on every reading from that beacon.

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:

How a fixed RSSI noise band widens into distance error with range A single rising curve. Holding the RSSI measurement noise fixed at plus or minus four decibels, the implied distance band is under one metre wide at one metre range, about two and a half metres wide at five metres, and over twelve metres wide at twenty-five metres. The relationship is multiplicative rather than additive, because the path-loss model is logarithmic in distance. Constant dB noise, wildly non-constant distance error 0 5 10 15 20 5 10 15 20 25 true distance (m) width of the implied distance band (m)

Range error is multiplicative. The same noise that costs centimetres at 1 m costs metres at 20 m — so a trilateration solve must weight near beacons far more heavily than far ones, rather than treating all ranges as equally trustworthy.

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.

This page is a companion to BLE Beacon Positioning & Trilateration, part of the Indoor Positioning & Fingerprinting reference.