Iris Classon
Iris Classon - In Love with Code

Approximating BLE Proximity with RSSI in .NET on iOS

I’ve continued working on my hobby project, a Bluetooth device emulator, and at some point I had what felt like a brilliant idea. I wanted to map out nearby Bluetooth devices from the receiving end and get a sense of where they were located.

That turns out not to be a straightforward problem. Bluetooth doesn’t tell you where something is. What you get is signal strength, and distance is just one of many factors that affect it.

Still, it felt like there had to be a way. I started playing around with this mostly for fun, knowing that I wouldn’t be able to produce a reliable distance estimate good enough for real-world use. There are simply too many variables that influence the signal.

But for the sake of curiosity, I wanted to see how far I could get.

RSSI

RSSI, Received Signal Strength Indicator, is a measure of how strong a received radio signal is. In BLE it is expressed in negative dBm values:

  • closer to 0 means stronger signal
  • more negative means weaker signal

So -50 is stronger than -80.

The important detail is that RSSI is not a distance. It is affected by distance, but also by a number of other factors that can easily dominate the reading.

What affects RSSI the most

Obstacles and materials

Walls, metal, and even people can significantly reduce signal strength. The human body is mostly water, which absorbs 2.4 GHz signals very effectively. Simply putting a phone in your pocket can change RSSI noticeably.

Indoor reflections and multipath

Indoors, signals bounce off surfaces and arrive via multiple paths. Some reinforce each other, others cancel out. This creates rapid fluctuations in RSSI even when nothing is moving.

Distance and transmit power

Distance still matters, and so does transmit power, but they are only part of the picture. Two identical distances can produce very different RSSI values depending on the environment.

The distance formula

There is a well-known formula to estimate distance:

distance ≈ 10 ^ ((TxPowerAt1m - RSSI) / (10 * n))

It works in theory, but in practice it depends on two values you rarely know precisely:

  • RSSI at exactly 1 meter for your specific devices
  • the environment factor n, which changes depending on walls, furniture, and people

You can calibrate these values, but even then the estimate will drift as soon as conditions change.

The result is that distance calculations tend to look precise but behave inconsistently.

The math behind it

The formula is based on a simplified radio propagation model called the log-distance path loss model, which in turn is derived from the Friis transmission equation.

The key idea is that signal strength does not decrease linearly with distance. It decreases logarithmically as the signal spreads out.

In practice, engineers express signal strength in decibels, which turns the relationship into a linear equation:

RSSI = TxPowerAt1m - 10 * n * log10(d)

Where:

  • d is distance
  • TxPowerAt1m is the expected RSSI at 1 meter
  • n is the path-loss exponent

If you rearrange that equation to solve for distance, you get:

d = 10 ^ ((TxPowerAt1m - RSSI) / (10 * n))

That is the formula we started with.

What it is really doing is estimating how far away a device would be if the signal behaved in a smooth, predictable way. Indoors, that assumption rarely holds.

Proximity, not distance

Instead of asking “how many meters away is this device,” a more reliable question is: is it very close, nearby, or far away?

That maps much better to what RSSI can actually tell you.

A simple set of buckets might look like this:

  • >= -60 → immediate
  • -60 to -75 → near
  • -75 to -88 → far
  • < -88 → unknown

These values are not universal. You will need to tune them based on your environment and hardware.

Working with Core Bluetooth in .NET

When scanning, you get RSSI from the discovery callback. When connected, you explicitly request updates.

public override void DiscoveredPeripheral(
    CBCentralManager central,
    CBPeripheral peripheral,
    NSDictionary advertisementData,
    NSNumber RSSI)
{
    if (RSSI != null)
    {
        var value = RSSI.Int32Value;
        // feed into estimator
    }
}

For connected devices:

peripheral.ReadRSSI();

And handle the callback:

public override void RssiRead(CBPeripheral peripheral, NSNumber rssi, NSError error)
{
    if (error != null || rssi == null)
        return;

    var value = rssi.Int32Value;
    // feed into estimator
}

Smoothing RSSI

Raw RSSI is too unstable to use directly. You need to smooth it.

A simple and effective approach is an exponential moving average:

_ema = alpha * rssi + (1 - alpha) * _ema;

Where alpha is typically between 0.2 and 0.3.

You should also ignore obvious outliers. If a value suddenly jumps by 15 dB compared to your current average, it is often just noise.

Instead of switching proximity immediately, require a few consecutive readings before changing state. This stabilizes the experience significantly.

A practical estimator

Putting it together:

public enum BleProximity
{
    Unknown,
    Immediate,
    Near,
    Far
}

public sealed class BleRssiEstimator
{
    private double? _ema;
    private readonly double _alpha = 0.25;
    private BleProximity _current = BleProximity.Unknown;
    private BleProximity _candidate;
    private int _candidateHits;

    public void AddSample(int rssi)
    {
        if (rssi >= 0)
            return;

        if (_ema.HasValue && Math.Abs(rssi - _ema.Value) > 15)
            return;

        _ema = _ema.HasValue
            ? _alpha * rssi + (1 - _alpha) * _ema.Value
            : rssi;

        UpdateProximity();
    }

    public BleProximity Current => _current;

    private void UpdateProximity()
    {
        if (!_ema.HasValue)
            return;

        var next = Classify(_ema.Value);

        if (next == _current)
        {
            _candidateHits = 0;
            return;
        }

        if (next != _candidate)
        {
            _candidate = next;
            _candidateHits = 1;
            return;
        }

        if (++_candidateHits >= 3)
        {
            _current = next;
            _candidateHits = 0;
        }
    }

    private static BleProximity Classify(double rssi)
    {
        if (rssi >= -60) return BleProximity.Immediate;
        if (rssi >= -75) return BleProximity.Near;
        if (rssi >= -88) return BleProximity.Far;
        return BleProximity.Unknown;
    }
}

Calibration matters

If you want reasonable behavior, you need to calibrate:

  1. Place devices 1 meter apart
  2. Measure RSSI for 10–20 seconds
  3. Average the result
  4. Adjust your thresholds based on real-world testing

Even small changes in placement or environment can shift results noticeably, so calibration is not a one-time task.

RSSI works best when you:

  • treat it as a relative signal, not a precise measurement
  • smooth aggressively
  • use proximity buckets instead of meters

Comments

Leave a comment below, or by email.

Last modified on 2026-02-20

comments powered by Disqus