Python  

Smoothing Drone Telemetry in Real Time with Exponential Moving Average Using Python

Table of Contents

  • Introduction

  • What Is an Exponential Moving Average (EMA)?

  • Why EMA Matters for Drone Flight Stability

  • The Problem with Raw Sensor Data

  • Efficient Online EMA Implementation

  • Complete Code with Live Drone Simulation

  • Best Practices for Embedded and Real-Time Systems

  • Conclusion

Introduction

Drones rely on a flood of sensor data—accelerometers, gyroscopes, barometers—to stay stable in mid-air. But raw readings are noisy, jittery, and prone to spikes from vibration, wind gusts, or electromagnetic interference. If a flight controller reacts to every erratic blip, the drone wobbles, drifts, or crashes.

Enter the Exponential Moving Average (EMA): a lightweight, real-time smoothing technique that filters noise while preserving responsiveness. Unlike simple averages, EMA gives more weight to recent data—perfect for dynamic systems like drones.

This article shows you how to implement EMA correctly for live telemetry, with a realistic simulation and production-ready code.

What Is an Exponential Moving Average (EMA)?

The EMA is a recursive filter defined as:

11

Where

  • xt​ is the current sensor reading,

  • α (alpha) is the smoothing factor (0 < α ≤ 1),

  • A higher α means more responsiveness (less smoothing),

  • A lower α means more stability (more smoothing).

Crucially, EMA requires only the previous EMA value and the new reading—making it ideal for memory-constrained microcontrollers.

Why EMA Matters for Drone Flight Stability

Imagine a delivery drone flying through a city canyon. Its barometer reports altitude, but turbulence causes rapid ±2-meter fluctuations—even when the drone is level.

Without smoothing:

  • The flight controller thinks it’s ascending/descending

  • It overcorrects with motor thrust

  • The drone oscillates violently

With EMA:

  • Short-term noise is suppressed

  • Real altitude trends are preserved

  • Flight becomes smooth and predictable

PlantUML Diagram

In real-world drone firmware (like PX4 or ArduPilot), EMA and its cousins (like complementary filters) are used extensively for sensor fusion.

The Problem with Raw Sensor Data

Raw IMU (Inertial Measurement Unit) data often contains:

  • High-frequency vibration noise (from motors)

  • Outliers (e.g., sudden spike from RF interference)

  • Quantization errors (limited sensor resolution)

A simple moving average would require storing a window of past values—costly on a 32KB RAM microcontroller. EMA solves this with O(1) memory and O(1) computation per sample.

Efficient Online EMA Implementation

We initialize EMA with the first reading (or a known baseline), then update it recursively. No arrays. No history. Just two variables: current EMA and alpha.

Edge cases to handle:

  • First reading (no prior EMA)

  • Invalid or NaN sensor values

  • Tunable alpha for different sensors (e.g., smoother for altitude, faster for orientation)

Complete Code with Live Drone Simulation

PlantUML Diagram
import math
import random
import time
from typing import Optional

class EMASmoother:
    """
    Real-time Exponential Moving Average for sensor smoothing.
    Updates in O(1) time and O(1) space per new data point.
    Ideal for drones, robotics, IoT, and embedded systems.
    """
    def __init__(self, alpha: float = 0.3):
        # Alpha (α) is the smoothing factor. Higher α means less smoothing.
        if not (0 < alpha <= 1):
            raise ValueError("Alpha must be in (0, 1]")
        self.alpha = alpha
        self._ema: Optional[float] = None # Use Optional for type hinting the initial None state
        self._initialized = False

    def update(self, value: float) -> float:
        """Update EMA with new sensor reading and return smoothed value."""
        # Standard EMA formula implementation
        if self._initialized:
            # EMA_t = alpha * Value_t + (1 - alpha) * EMA_{t-1}
            self._ema = self.alpha * value + (1 - self.alpha) * self._ema
        else:
            # On first call, initialize EMA to the first reading
            self._ema = value
            self._initialized = True
            
        # The return type is float because _ema is guaranteed to be float if _initialized is True
        return self._ema  # type: ignore 

    def reset(self) -> None:
        """Reset EMA state (e.g., on sensor recalibration)."""
        self._ema = None
        self._initialized = False

    @property
    def ema(self) -> float:
        """Return the current smoothed value, or 0.0 if not yet initialized."""
        return self._ema if self._initialized and self._ema is not None else 0.0


# --- Live Simulation: Drone Altitude Stabilization ---

def simulate_drone_telemetry():
    """Simulate noisy barometer readings and EMA smoothing."""
    
    # 1. Aggressive EMA (α=0.8): Responds quickly to changes, but passes more noise.
    #    Weighted more towards current reading (80%).
    ema_fast = EMASmoother(alpha=0.8) 
    
    # 2. Smooth EMA (α=0.1): Responds slowly, excellent noise rejection.
    #    Weighted more towards old EMA (90%).
    ema_smooth = EMASmoother(alpha=0.1) 
    
    target_alt = 50.0  # meters
    print("Drone Altitude Stabilization (Target: 50.0m)")
    print("-" * 85)
    print(f"{'Sec':>3} | {'True Alt':>8} | {'Raw Alt':>8} | {'Fast EMA (α=0.8)':>15} | {'Smooth EMA (α=0.1)':>17} | {'Observation':<10}")
    print("-" * 85)
    
    for second in range(1, 31):
        # Simulate true altitude with minor drift
        true_alt = target_alt + random.uniform(-0.1, 0.1)
        
        # Add high-frequency noise
        raw_alt = true_alt + random.gauss(0, 1.5)
        
        observation = ""
        # Inject a transient signal (e.g., a rapid maneuver or wind gust)
        if 10 <= second <= 12:
            raw_alt += 3.5  # Sudden high reading
            observation = "⬆ SPIKE"
        elif 20 <= second <= 22:
            raw_alt -= 3.0  # Sudden low reading
            observation = "⬇ DIP"
        
        smooth_alt_fast = ema_fast.update(raw_alt)
        smooth_alt_slow = ema_smooth.update(raw_alt)
        
        print(f"{second:3d} | {true_alt:8.2f} | {raw_alt:8.2f} | {smooth_alt_fast:15.2f} | {smooth_alt_slow:17.2f} | {observation}")
        time.sleep(0.1)
    
    print("-" * 85)
    print("\nInsight: The **Fast EMA (α=0.8)** tracks the true altitude and responds to the spike/dip quickly, but it also carries more noise. The **Smooth EMA (α=0.1)** effectively rejects the noise and dampens the spike/dip, providing a more stable signal for control systems.")


if __name__ == "__main__":
    simulate_drone_telemetry()
1

Why this works in production:

  • No external dependencies

  • Handles first-sample initialization correctly

  • Alpha is configurable per sensor type

  • Reset method supports recalibration

  • Float-safe and efficient

Best Practices for Embedded and Real-Time Systems

  • Tune alpha empirically: Start with α = 0.2–0.4 for altitude, 0.5–0.7 for fast-changing angles.

  • Validate inputs: Reject NaN/inf before updating.

  • Use fixed-point math on microcontrollers without FPU (e.g., scale alpha to integer).

  • Combine with outlier rejection: Discard readings beyond 3σ before EMA.

  • Log raw vs smoothed: Helps debug flight anomalies post-mission.

Conclusion

In drone telemetry—and any real-time sensor system—clean data is non-negotiable. The Exponential Moving Average delivers maximum stability with minimum overhead, making it a staple in aerospace, robotics, and industrial control. With just two lines of update logic, you transform jittery chaos into smooth, actionable signals. Whether you’re coding for a Raspberry Pi drone or a commercial UAV, EMA is your first line of defense against noise. Smooth your data. Stabilize your system. Fly with confidence.