
RSSI-based distance estimation is the foundation of every Bluetooth Beacon deployment, yet raw RSSI readings are notoriously noisy. A beacon placed 3 meters away can produce readings swinging from -75 dBm to -55 dBm within seconds, driven by multipath fading, body shadowing, and RF interference. Without proper filtering, proximity detection becomes a coin flip and positioning errors easily exceed 5 meters. This article walks through the filtering algorithms that actually work in production beacon systems—from simple moving averages to Kalman and particle filters—with real parameters, trade-off tables, and implementation guidance.
## 1. The RSSI Noise Problem
RSSI (Received Signal Strength Indicator) measures the power level of a received Bluetooth advertisement packet. In free space, RSSI follows a predictable path-loss model:
RSSI(d) = TX_power - 10*n*log10(d/d0) - X_sigma<br />
Where n is the path-loss exponent (2.0 in free space, 2.7-4.3 indoors), d0 is the reference distance (1 m), and X_sigma is a zero-mean Gaussian random variable representing shadowing, with standard deviation σ = 4-8 dB in typical indoor environments.
In practice, the Gaussian assumption is an approximation. Real RSSI distributions are better described as a mixture of two components:
– Line-of-sight (LOS) component: When a direct path exists, RSSI is relatively stable (σ ≈ 2-3 dB).
– Non-line-of-sight (NLOS) component: When the direct path is blocked, RSSI drops by 10-20 dB and becomes highly variable (σ ≈ 6-10 dB).
This bimodal behavior is the root cause of “jumpy” distance estimates. A person walking between the beacon and receiver can cause instantaneous RSSI drops of 15 dB, which translates to an apparent distance change from 3 m to 17 m under the log-loss model.
### Measured RSSI characteristics
| Environment | Path-loss exponent (n) | Shadowing σ (dB) | LOS/NLOS ratio |
|—|—|—|—|
| Open office (cubicle) | 2.3 | 3.8 | 85/15 |
| Corridor (narrow) | 2.0 | 2.5 | 90/10 |
| Mixed office (partitions) | 2.7 | 5.2 | 60/40 |
| Warehouse (metal racks) | 3.1 | 7.0 | 45/55 |
| Industrial (machinery) | 3.4 | 8.1 | 35/65 |
The warehouse and industrial environments show why off-the-shelf beacon positioning systems often fail: more than half the readings are NLOS, and σ exceeds 7 dB.
## 2. Why You Need Filtering: The Cost of Raw RSSI
Consider a proximity detection application with a 4-meter trigger threshold. Using raw RSSI with n=2.7 and σ=5.2 dB:
– At 3 m (inside zone): P(RSSI < threshold) ≈ 18% → false negative rate
– At 5 m (outside zone): P(RSSI ≥ threshold) ≈ 12% → false positive rate
A 30% combined error rate makes raw RSSI unusable for any reliable proximity trigger. Filtering reduces the effective σ by a factor of √N for N-sample averaging, but this comes at the cost of latency.
The fundamental trade-off:
Stability (reduced σ) ⟷ Latency (delayed response)
Every filter discussed below sits somewhere on this trade-off curve. The art is choosing the right point for your application.
## 3. Simple Moving Average (SMA)
The simplest approach: maintain a sliding window of the last N RSSI samples and output their arithmetic mean.
#define WINDOW_SIZE 8
typedef struct {
int8_t buffer[WINDOW_SIZE];
uint8_t index;
uint8_t count;
int32_t sum;
} sma_filter_t;
int8_t sma_update(sma_filter_t *f, int8_t rssi) {
// Remove oldest sample from sum
if (f->count == WINDOW_SIZE) {<br />
f->sum -= f->buffer[f->index];<br />
} else {<br />
f->count++;<br />
}<br />
// Add new sample<br />
f->buffer[f->index] = rssi;<br />
f->sum += rssi;<br />
f->index = (f->index + 1) % WINDOW_SIZE;<br />
return (int8_t)(f->sum / f->count);<br />
}<br />
Characteristics:
| Parameter | Value |
|—|—|
| Effective σ reduction | √N (8 samples → 2.83× reduction → σ_eff = σ/2.83) |
| Group delay | (N-1)/2 × sampling_interval |
| Memory | N bytes |
| Compute per sample | O(1) (with running sum) |
With N=8 and a 100 ms advertisement interval, the group delay is 350 ms. This is acceptable for proximity detection but too slow for real-time positioning.
Problem: SMA gives equal weight to all samples. A 200 ms old reading has the same influence as the newest one, which makes SMA sluggish when the beacon actually moves.
## 4. Weighted Moving Average (WMA)
WMA assigns higher weights to recent samples, improving responsiveness:
// Linear weights: w[i] = i+1, total weight = N*(N+1)/2<br />
int8_t wma_update(sma_filter_t *f, int8_t rssi) {<br />
// Shift buffer and add new sample<br />
for (int i = WINDOW_SIZE - 1; i > 0; i--) {<br />
f->buffer[i] = f->buffer[i-1];<br />
}<br />
f->buffer[0] = rssi;
int32_t weighted_sum = 0;<br />
for (int i = 0; i < WINDOW_SIZE; i++) {
weighted_sum += f->buffer[i] * (WINDOW_SIZE - i);<br />
}<br />
return (int8_t)(weighted_sum / (WINDOW_SIZE * (WINDOW_SIZE + 1) / 2));<br />
}<br />
WMA reduces group delay by approximately 30% compared to SMA for the same window size, at the cost of slightly less noise reduction (effective N is reduced by ~15%).
| Filter | N=8 σ reduction | Group delay (100ms interval) | Responsiveness |
|—|—|—|—|
| SMA | 2.83× | 350 ms | Low |
| WMA | 2.45× | 240 ms | Medium |
## 5. Exponential Weighted Moving Average (EWMA)
EWMA is the most popular RSSI filter in commercial beacon SDKs (including Apple iBeacon, Google Eddystone) because it requires only one state variable and has no buffer:
typedef struct {<br />
float filtered;<br />
float alpha;<br />
uint8_t initialized;<br />
} ewma_filter_t;
float ewma_update(ewma_filter_t *f, float rssi) {<br />
if (!f->initialized) {<br />
f->filtered = rssi;<br />
f->initialized = 1;<br />
} else {<br />
f->filtered = f->alpha * rssi + (1.0f - f->alpha) * f->filtered;<br />
}<br />
return f->filtered;<br />
}<br />
The smoothing factor α controls the trade-off:
– α = 1.0: No filtering (raw RSSI)
– α = 0.3: Moderate smoothing (equivalent to N≈6 SMA)
– α = 0.1: Heavy smoothing (equivalent to N≈20 SMA)
– α = 0.05: Very heavy (equivalent to N≈40 SMA)
The “equivalent N” for EWMA is approximately N_eq ≈ (2-α)/α, which gives the same variance reduction as an N-sample SMA.
### Choosing α
| Application | Recommended α | Settling time | Effective σ |
|—|—|—|—|
| Real-time positioning (asset tracking) | 0.3-0.4 | 0.3-0.5 s | σ/2.0 |
| Proximity detection (push notifications) | 0.15-0.25 | 0.8-1.5 s | σ/3.5 |
| Indoor navigation (wayfinding) | 0.2-0.3 | 0.5-1.0 s | σ/2.8 |
| Static monitoring (occupancy sensing) | 0.05-0.1 | 2-4 s | σ/5.0 |
The settling time is approximately 5/α × interval. For α=0.2 at 100 ms interval, the filter takes about 2.5 seconds to converge to a new steady state after a step change.
EWMA is not symmetric: it responds quickly to RSSI increases (beacon approaching) but slowly to RSSI decreases (beacon departing), because the filter always lags. For proximity enter/exit detection, this asymmetry causes “sticky exits”—the system keeps reporting the beacon as nearby for several seconds after it has moved away.
## 6. Median Filter for Outlier Rejection
Median filters are fundamentally different from averaging: they output the median of the window, not the mean. This makes them immune to outliers.
// Simple insertion sort for small N<br />
int8_t median_filter(int8_t *buf, uint8_t n) {<br />
int8_t temp[n];<br />
memcpy(temp, buf, n);<br />
// Insertion sort<br />
for (int i = 1; i < n; i++) {
int8_t key = temp[i];
int j = i - 1;
while (j >= 0 && temp[j] > key) {<br />
temp[j+1] = temp[j];<br />
j--;<br />
}<br />
temp[j+1] = key;<br />
}<br />
return temp[n / 2];<br />
}<br />
Why median works for RSSI: The bimodal LOS/NLOS distribution means that NLOS readings are essentially outliers—sudden drops of 10-20 dB. A mean-based filter incorporates these outliers, biasing the estimate low (farther distance). A median filter with N≥5 simply discards them.
| Filter type | Input: [-70, -72, -55, -71, -68, -73, -70] | Output | True RSSI ≈ -71 |
|—|—|—|—|
| SMA (N=7) | Mean = -68.4 | -68.4 dBm | 2.6 dB bias |
| EWMA (α=0.3) | | -67.8 dBm | 3.2 dB bias |
| Median (N=7) | Sorted: [-73,-72,-71,-70,-68,-55] | -70 dBm | 1.0 dB bias |
The -55 dBm outlier (likely a multipath reflection) creates a 2-3 dB positive bias in mean-based filters, which translates to a 25% distance underestimate. The median filter is barely affected.
Hybrid approach: Apply a median filter (N=5) first to reject outliers, then EWMA (α=0.2) on the median output for smoothing. This two-stage design combines outlier rejection with low-latency tracking, and is the recommended approach for production systems.
## 7. Kalman Filter for RSSI Estimation
The Kalman filter models RSSI as a state with process noise and measurement noise, producing the minimum mean-square error (MMSE) estimate under linear Gaussian assumptions.
### State model
State: x[k] = x[k-1] + w[k] (constant RSSI + process noise)<br />
Measurement: z[k] = x[k] + v[k] (observed RSSI + measurement noise)
Process noise: w ~ N(0, Q)<br />
Measurement noise: v ~ N(0, R)<br />
For a stationary beacon, Q should be small (0.1-0.5) since RSSI doesn’t change rapidly. For a moving beacon, Q should be larger (1.0-5.0) to allow the filter to track changes.
### Implementation
typedef struct {<br />
float x; // State estimate<br />
float p; // Estimation uncertainty<br />
float q; // Process noise variance<br />
float r; // Measurement noise variance<br />
} kalman_rssi_t;
float kalman_update(kalman_rssi_t *kf, float measurement) {<br />
// Predict<br />
kf->p = kf->p + kf->q;
// Update<br />
float k = kf->p / (kf->p + kf->r); // Kalman gain<br />
kf->x = kf->x + k * (measurement - kf->x);<br />
kf->p = (1.0f - k) * kf->p;
return kf->x;<br />
}<br />
### Tuning Q and R
The ratio Q/R determines filter behavior:
| Q/R ratio | Behavior | Application |
|—|—|—|
| 0.01 | Very smooth, slow response | Static asset monitoring |
| 0.1 | Balanced | General indoor positioning |
| 0.5 | Responsive, moderate noise | Moving asset tracking |
| 1.0+ | Little filtering, fast response | High-mobility scenarios |
With R set from measured σ² (e.g., σ=5.2 → R=27.0) and Q=2.7 (Q/R=0.1), the Kalman filter achieves a σ_eff of approximately 2.3 dB—a 2.3× reduction—with a settling time of about 1.5 seconds.
### Advantages over EWMA
The Kalman filter adapts its gain dynamically: when the estimation uncertainty is high (just after initialization or a step change), the gain is high and the filter responds quickly. As confidence builds, the gain decreases and the filter becomes smoother. EWMA uses a fixed α, which cannot adapt.
| Property | EWMA (α=0.2) | Kalman (Q=2.7, R=27.0) |
|—|—|—|
| Steady-state σ_eff | 2.6 dB | 2.3 dB |
| Step response (90% rise) | 2.5 s | 1.2 s |
| Adaptive gain | No | Yes |
| Memory | 4 bytes | 16 bytes |
| Compute per sample | 2 ops | 5 ops |
## 8. Particle Filter for Non-Linear Environments
In environments with severe multipath (warehouses, factories), the Gaussian assumption breaks down. RSSI distributions become multimodal, and linear filters (EWMA, Kalman) converge to the wrong value.
The particle filter represents the RSSI probability distribution as a set of weighted samples (particles):
import numpy as np
class ParticleFilterRSSI:<br />
def __init__(self, n_particles=100, rssi_range=(-100, -30)):<br />
self.n = n_particles<br />
self.particles = np.random.uniform(rssi_range[0], rssi_range[1], n_particles)<br />
self.weights = np.ones(n_particles) / n_particles
def predict(self, process_std=2.0):<br />
'''Add process noise'''<br />
self.particles += np.random.normal(0, process_std, self.n)
def update(self, measurement, measurement_std=5.0):<br />
'''Weight particles by likelihood'''<br />
likelihood = np.exp(-0.5 * ((self.particles - measurement) / measurement_std) ** 2)<br />
self.weights *= likelihood<br />
self.weights /= self.weights.sum() # Normalize
def estimate(self):<br />
'''Weighted mean'''<br />
return np.sum(self.particles * self.weights)
def resample(self):<br />
'''Systematic resampling to prevent particle degeneracy'''<br />
indices = np.random.choice(self.n, self.n, p=self.weights)<br />
self.particles = self.particles[indices]<br />
self.weights.fill(1.0 / self.n)
def step(self, measurement):<br />
self.predict()<br />
self.update(measurement)<br />
est = self.estimate()<br />
# Resample if effective particle count is low<br />
n_eff = 1.0 / np.sum(self.weights ** 2)<br />
if n_eff < self.n / 2:
self.resample()
return est
### Performance in NLOS-dominant environments
In a warehouse test (55% NLOS), tracking a beacon at 4 m distance:
| Filter | Mean error (m) | P95 error (m) | Convergence time |
|---|---|---|---|
| SMA (N=8) | 2.1 | 5.8 | 0.8 s |
| EWMA (α=0.2) | 1.9 | 5.2 | 1.0 s |
| Kalman (Q/R=0.1) | 1.6 | 4.5 | 1.2 s |
| Particle (100 particles) | 0.9 | 2.1 | 1.5 s |
The particle filter reduces mean error by 43% compared to EWMA in this harsh environment. However, it requires 100× more memory and compute, making it impractical on resource-constrained beacon tags.
Recommendation: Use particle filters on the receiver/gateway side (smartphones, Raspberry Pi gateways) where compute is available. On beacon tags themselves, EWMA or Kalman is sufficient since the tag only broadcasts—the filtering happens at the receiver.
## 9. Filter Comparison: Comprehensive Trade-off Table
| Filter | σ reduction (σ=5.2 dB) | Latency | Memory | Compute | Outlier resistant | Adaptive |
|---|---|---|---|---|---|---|
| Raw RSSI | 1.0× (5.2 dB) | 0 ms | 0 B | 0 ops | No | N/A |
| SMA (N=4) | 2.0× (2.6 dB) | 150 ms | 4 B | 1 op | No | No |
| SMA (N=8) | 2.8× (1.8 dB) | 350 ms | 8 B | 1 op | No | No |
| WMA (N=8) | 2.5× (2.1 dB) | 240 ms | 8 B | 8 ops | No | No |
| EWMA (α=0.3) | 1.8× (2.9 dB) | 100 ms | 4 B | 2 ops | No | No |
| EWMA (α=0.15) | 2.9× (1.8 dB) | 800 ms | 4 B | 2 ops | No | No |
| Median (N=5) | 1.7× (3.1 dB) | 200 ms | 5 B | 10 ops | Yes | No |
| Median+EWMA | 2.7× (1.9 dB) | 300 ms | 9 B | 12 ops | Yes | No |
| Kalman | 2.3× (2.3 dB) | 150 ms | 16 B | 5 ops | Partial | Yes |
| Particle (100) | 3.5× (1.5 dB) | 400 ms | 800 B | 300 ops | Yes | Yes |
Key observations:
– Median+EWMA offers the best balance for embedded systems: near-Kalman performance with simpler implementation and outlier rejection.
– Kalman is optimal when you can tune Q/R properly and need adaptive behavior.
– Particle filter excels in harsh multipath but is gateway-only due to compute cost.
– SMA is adequate for static monitoring where latency doesn’t matter.
## 10. Adaptive Filtering Strategies
Fixed-parameter filters are suboptimal because RSSI statistics change with environment and beacon mobility. Adaptive strategies improve performance significantly:
### 10.1 Mobility-aware EWMA
Detect beacon movement using RSSI variance over a short window, then adjust α dynamically:
float adaptive_alpha(float *recent_rssi, uint8_t n) {
// Compute short-term variance
float mean = 0;
for (int i = 0; i < n; i++) mean += recent_rssi[i];
mean /= n;
float var = 0;
for (int i = 0; i < n; i++) {
float d = recent_rssi[i] - mean;
var += d * d;
}
var /= n;
// High variance → likely moving → increase α (more responsive)
// Low variance → likely static → decrease α (more smoothing)
if (var > 25.0f) return 0.4f; // Moving fast<br />
else if (var > 10.0f) return 0.25f; // Moving slowly<br />
else return 0.1f; // Static<br />
}<br />
### 10.2 Environment-aware Kalman
Adjust R based on the local NLOS probability. When recent measurements show large deviations from the predicted state, increase R temporarily:
void adaptive_kalman(kalman_rssi_t *kf, float measurement, float *residuals, uint8_t n) {<br />
// Compute innovation (residual) statistics<br />
float mean_res = 0;<br />
for (int i = 0; i < n; i++) mean_res += residuals[i];
mean_res /= n;
float res_var = 0;
for (int i = 0; i < n; i++) {
float d = residuals[i] - mean_res;
res_var += d * d;
}
res_var /= n;
// Inflate R when residuals are large (likely NLOS)
if (res_var > kf->r * 1.5f) {<br />
kf->r = kf->r * 1.3f; // Inflate measurement noise<br />
} else {<br />
kf->r = kf->r * 0.95f; // Gradually restore<br />
}<br />
// Clamp R to reasonable range<br />
if (kf->r < 5.0f) kf->r = 5.0f;<br />
if (kf->r > 100.0f) kf->r = 100.0f;
kalman_update(kf, measurement);<br />
}<br />
### 10.3 HMM-based LOS/NLOS detection
A Hidden Markov Model can classify each measurement as LOS or NLOS, then apply different filters:
– LOS state: Apply Kalman with low R (trusting the measurement)
– NLOS state: Apply Kalman with high R (discounting the measurement) or discard it entirely
The transition matrix is empirically learned from training data. Typical transition probabilities for an office environment:
| | Next: LOS | Next: NLOS |
|—|—|—|
| Current: LOS | 0.92 | 0.08 |
| Current: NLOS | 0.35 | 0.65 |
This means LOS tends to persist (92% chance of staying LOS), while NLOS is also somewhat persistent (65%). The HMM achieves 85-90% classification accuracy, significantly improving filter performance in mixed environments.
## 11. Implementation Considerations
### 11.1 Integer arithmetic for constrained MCUs
On 8-bit or 16-bit MCUs (common in beacon tags), floating-point operations are expensive. EWMA can be implemented in fixed-point:
// Fixed-point EWMA with Q8 format (8 fractional bits)<br />
// alpha = 0.2 → alpha_q8 = 51 (0.2 * 256)<br />
typedef struct {<br />
int16_t filtered; // Q8 fixed point<br />
uint8_t initialized;<br />
} ewma_fixed_t;
int8_t ewma_fixed_update(ewma_fixed_t *f, int8_t rssi) {<br />
int16_t rssi_q8 = (int16_t)rssi << 8;
if (!f->initialized) {<br />
f->filtered = rssi_q8;<br />
f->initialized = 1;<br />
} else {<br />
// filtered = 0.2*rssi + 0.8*filtered<br />
f->filtered = (51 * rssi_q8 + 205 * f->filtered) >> 8;<br />
}<br />
return (int8_t)(f->filtered >> 8);<br />
}<br />
This uses only integer multiply and shift—no floating-point library needed.
### 11.2 Handling advertisement intervals
Beacon advertisement intervals vary from 20 ms (high-rate positioning) to 1000 ms (battery-saving mode). Filter parameters must scale with interval:
– At 100 ms interval with α=0.2: time constant = 0.5 s
– At 1000 ms interval with α=0.2: time constant = 5.0 s (too slow)
Solution: Compute α from a desired time constant τ:
α = 1 - exp(-Δt / τ)<br />
Where Δt is the advertisement interval and τ is the desired time constant (e.g., 0.5 s). This ensures consistent filter behavior regardless of interval.
### 11.3 Multi-beacon filtering
When tracking multiple beacons simultaneously, each beacon needs its own filter instance. Memory budget on a typical gateway (ESP32, 520 KB SRAM):
| Filter type | Memory per beacon | Max beacons (50 KB budget) |
|—|—|—|
| EWMA (float) | 8 B | 6,250 |
| Kalman (float) | 32 B | 1,562 |
| Median+EWMA (N=5, int8) | 14 B | 3,571 |
| Particle (100, float) | 1,600 B | 31 |
EWMA and Kalman scale well to thousands of beacons. Particle filters are limited to dozens.
## 12. Deployment Recommendations
Based on field testing across office, retail, warehouse, and industrial environments, here are the recommended filter configurations:
### Proximity detection (push notifications, zone entry/exit)
Filter: Median (N=5) + EWMA (α=0.15)<br />
Interval: 100-200 ms<br />
σ_eff: ~2.0 dB<br />
Latency: ~400 ms<br />
Notes: Median rejects NLOS outliers; EWMA smooths remaining noise.<br />
Use hysteresis on the distance threshold (enter at 3m, exit at 5m)<br />
to prevent bounce.<br />
### Indoor positioning (trilateration, fingerprinting)
Filter: Kalman (Q=0.5, R=σ_measured²)<br />
Interval: 100-300 ms<br />
σ_eff: ~2.3 dB<br />
Latency: ~150 ms<br />
Notes: Tune R per environment. Use adaptive R for NLOS-heavy areas.<br />
Position update rate should be 2-5 Hz for smooth UI.<br />
### Asset tracking (RTLS, forklift tracking)
Filter: Adaptive EWMA (α: 0.1-0.4, mobility-aware)<br />
Interval: 200-500 ms<br />
σ_eff: ~2.5 dB (dynamic)<br />
Latency: 100-800 ms (dynamic)<br />
Notes: Detect movement via RSSI variance, switch α accordingly.<br />
Use particle filter on gateway if multipath is severe.<br />
### Static monitoring (occupancy sensing, room-level presence)
Filter: EWMA (α=0.05) or SMA (N=20)<br />
Interval: 500-1000 ms<br />
σ_eff: ~1.2 dB<br />
Latency: 2-5 s<br />
Notes: Latency is acceptable for presence detection.<br />
Ultra-low σ enables reliable room-level classification.<br />
### Severe multipath (warehouse, factory)
Filter: Particle filter (50-100 particles) on gateway<br />
+ Kalman on mobile receiver<br />
Interval: 100-200 ms<br />
σ_eff: ~1.5 dB<br />
Latency: ~400 ms<br />
Notes: Particle filter handles multimodal RSSI distributions.<br />
Gateway-side processing offloads compute from tags.<br />
## Conclusion
RSSI filtering is not optional—it is the single most impactful signal processing step in any beacon system. The choice of algorithm depends on the application’s latency tolerance, compute budget, and RF environment:
– For quick prototypes and simple proximity: EWMA with α=0.2 is sufficient.
– For production proximity detection: Median + EWMA hybrid is the best general-purpose choice.
– For positioning systems: Kalman filter with environment-tuned Q/R.
– For harsh multipath: Particle filter on the gateway side.
The most common mistake is using a single fixed filter for all scenarios. Real environments change—people move, doors open, Wi-Fi channels shift. Adaptive strategies that detect mobility and NLOS conditions can reduce positioning error by 30-50% compared to fixed-parameter filters, with no additional hardware cost.
Remember that no filter can recover information that isn’t there. If the beacon only advertises once per second, no amount of filtering will give you sub-second latency. Start with an appropriate advertisement rate, choose the simplest filter that meets your accuracy requirements, and add complexity only when measurements prove it’s necessary. When selecting hardware, consider a reliable Bluetooth module that supports configurable advertisement intervals and output power—these parameters directly affect the raw RSSI quality that your filter has to work with.
