Skip to main content

Overview

The Sensor Module V2 framework provides advanced real-time filtering capabilities to smooth noisy sensor data, remove outliers, and improve measurement accuracy. Four filter algorithms are available, each optimized for different signal characteristics.

Filter Architecture

enum FilterType {
    FILTER_NONE,
    FILTER_MOVING_AVERAGE,
    FILTER_MEDIAN,
    FILTER_KALMAN,
    FILTER_EXPONENTIAL
};

struct FilterParams {
    union {
        struct { uint8_t windowSize; } movingAverage;
        struct { uint8_t windowSize; } median;
        struct {
            float processNoise;
            float measurementNoise;
            float estimateError;
        } kalman;
        struct { float alpha; } exponential;
    };
};

class SensorFilterV2 {
public:
    bool attachFilter(const char *sensorName, const char *valueKey,
                     FilterType type, FilterParams params);
    bool updateFilter(const char *sensorName, const char *valueKey,
                     FilterType type, FilterParams params);
    bool detachFilter(const char *sensorName, const char *valueKey);
    float getFilteredValue(const char *sensorName, const char *valueKey);
    void resetFilter(const char *sensorName, const char *valueKey);
};

Filter Types

Moving Average Filter

Averages the last N samples to smooth out noise. Best for signals with random noise. Algorithm:
filtered_value = (sample_1 + sample_2 + ... + sample_N) / N
Characteristics:
  • Simple and effective for random noise
  • Window size determines smoothing vs. responsiveness
  • Equal weight to all samples in window
  • Higher window size = smoother but slower response
Usage:
#define ENABLE_SENSOR_MODULE_V2
#define ENABLE_SENSOR_FILTER_V2
#define ENABLE_SENSOR_ANALOG_V2
#include "Kinematrix.h"

SensorModuleV2 sensors;
AnalogSensV2 noisySensor(A0, 3.3, 4095);

void setup() {
    sensors.addSensor("noisy", &noisySensor);
    sensors.init();
    
    // Attach moving average filter
    FilterParams params;
    params.movingAverage.windowSize = 10;  // Average last 10 samples
    sensors.attachFilter("noisy", "volt", FILTER_MOVING_AVERAGE, params);
}

void loop() {
    sensors.update();
    
    if (sensors.isUpdated("noisy")) {
        float raw = sensors.getValue<float>("noisy", "volt");
        float filtered = sensors.getFilteredValue("noisy", "volt");
        
        Serial.print("Raw: ");
        Serial.print(raw, 3);
        Serial.print(" V | Filtered: ");
        Serial.print(filtered, 3);
        Serial.println(" V");
    }
    
    delay(100);
}
Recommended Window Sizes:
  • Fast-changing signals: 3-5 samples
  • Medium signals: 5-10 samples
  • Slow-changing signals: 10-20 samples
  • Very slow signals: 20-50 samples

Median Filter

Returns the median value from the last N samples. Excellent for removing outliers and spikes. Algorithm:
Sort [sample_1, sample_2, ..., sample_N]
filtered_value = middle_value
Characteristics:
  • Excellent spike/outlier rejection
  • Preserves edges better than moving average
  • More computationally intensive (sorting required)
  • Odd window sizes work best
Usage:
FilterParams params;
params.median.windowSize = 5;  // Use median of last 5 samples
sensors.attachFilter("noisy", "volt", FILTER_MEDIAN, params);

// Example with ultrasonic sensor (prone to spikes)
UltrasonicSensV2 distance(9, 10);  // Trigger, Echo
sensors.addSensor("distance", &distance);

params.median.windowSize = 7;
sensors.attachFilter("distance", "distance_cm", FILTER_MEDIAN, params);
Recommended Window Sizes:
  • Occasional spikes: 3-5 samples
  • Frequent outliers: 5-9 samples
  • Heavy noise: 9-15 samples
When to Use:
  • Ultrasonic distance sensors (echoes, reflections)
  • Pulse detection (removing artifacts)
  • Digital communication (bit errors)
  • Any signal with frequent outliers

Kalman Filter

Optimal recursive filter that estimates the true value considering process and measurement uncertainty. Algorithm:
Prediction:
  x_predicted = x_previous
  P_predicted = P_previous + process_noise

Update:
  K = P_predicted / (P_predicted + measurement_noise)
  x_filtered = x_predicted + K * (measurement - x_predicted)
  P = (1 - K) * P_predicted
Characteristics:
  • Optimal for Gaussian noise
  • Adapts to signal dynamics
  • Very smooth output with minimal lag
  • Requires tuning of noise parameters
Usage:
FilterParams params;
params.kalman.processNoise = 0.01f;      // How much the signal changes
params.kalman.measurementNoise = 0.1f;   // Sensor measurement noise
params.kalman.estimateError = 0.1f;      // Initial estimate uncertainty
sensors.attachFilter("temp", "temperature", FILTER_KALMAN, params);

// Example with temperature sensor
DHTSensV2 dht(2, DHT22);
sensors.addSensor("temp", &dht);

FilterParams tempFilter;
tempFilter.kalman.processNoise = 0.001f;     // Temperature changes slowly
tempFilter.kalman.measurementNoise = 0.5f;   // DHT22 ±0.5°C accuracy
tempFilter.kalman.estimateError = 1.0f;
sensors.attachFilter("temp", "temperature", FILTER_KALMAN, tempFilter);
Parameter Tuning Guide: Process Noise (Q): How much the true value changes between measurements
  • Slow-changing signal (temperature): 0.001 - 0.01
  • Medium signal (pressure): 0.01 - 0.1
  • Fast-changing signal (accelerometer): 0.1 - 1.0
Measurement Noise (R): Sensor measurement uncertainty
  • High-quality sensor: 0.01 - 0.1
  • Medium-quality sensor: 0.1 - 1.0
  • Low-quality sensor: 1.0 - 10.0
Estimate Error (P): Initial uncertainty (usually 0.1 - 1.0) Tuning Strategy:
  1. Start with R = sensor datasheet accuracy
  2. Set Q = R / 10 for slow signals, Q = R for fast signals
  3. Set P = R
  4. Adjust Q: increase for faster response, decrease for smoother output

Exponential Moving Average (EMA)

Weighted average giving more importance to recent samples. Algorithm:
filtered_value = alpha * new_sample + (1 - alpha) * previous_filtered
Characteristics:
  • Memory efficient (only stores last value)
  • Simple and fast
  • Smooth response with adjustable lag
  • Alpha determines filter strength
Usage:
FilterParams params;
params.exponential.alpha = 0.1f;  // 0.0 (heavy filter) to 1.0 (no filter)
sensors.attachFilter("sensor", "value", FILTER_EXPONENTIAL, params);

// Example with gas sensor
MQSensV2 mq("ESP32", 3.3, 12, A0, "MQ-135");
sensors.addSensor("gas", &mq);

FilterParams gasFilter;
gasFilter.exponential.alpha = 0.2f;  // Smooth gas concentration readings
sensors.attachFilter("gas", "ppm", FILTER_EXPONENTIAL, gasFilter);
Alpha Values:
  • 0.05 - 0.1: Heavy smoothing, slow response
  • 0.1 - 0.3: Medium smoothing
  • 0.3 - 0.5: Light smoothing, fast response
  • 0.5 - 1.0: Minimal smoothing
Equivalent Window Size:
approximate_window = 2 / alpha - 1

alpha = 0.1  → ~19 samples
alpha = 0.2  → ~9 samples
alpha = 0.5  → ~3 samples

Filter Management

Attaching Filters

// Attach filter to sensor value
FilterParams params;
params.movingAverage.windowSize = 10;
bool success = sensors.attachFilter("temp", "temperature", 
                                    FILTER_MOVING_AVERAGE, params);

if (success) {
    Serial.println("Filter attached successfully");
} else {
    Serial.println("Failed to attach filter");
}

Updating Filter Parameters

// Change filter type or parameters
FilterParams newParams;
newParams.kalman.processNoise = 0.005f;
newParams.kalman.measurementNoise = 0.2f;
newParams.kalman.estimateError = 0.1f;

sensors.updateFilter("temp", "temperature", FILTER_KALMAN, newParams);

Detaching Filters

// Remove filter from specific value
sensors.detachFilter("temp", "temperature");

// Remove all filters
sensors.detachAllFilters();

Checking Filter Status

// Check if filter exists
if (sensors.hasFilter("temp", "temperature")) {
    FilterType type = sensors.getFilterType("temp", "temperature");
    Serial.print("Filter type: ");
    Serial.println(type);
}

Resetting Filters

// Reset specific filter (clears history)
sensors.resetFilter("temp", "temperature");

// Reset all filters
sensors.resetAllFilters();

Filter Comparison Example

#define ENABLE_SENSOR_MODULE_V2
#define ENABLE_SENSOR_FILTER_V2
#define ENABLE_SENSOR_ANALOG_V2
#include "Kinematrix.h"

SensorModuleV2 sensors;
AnalogSensV2 sensor(A0, 3.3, 4095);

void setup() {
    Serial.begin(115200);
    
    // Add sensor 4 times for comparison
    sensors.addSensor("raw", &sensor);
    sensors.addSensor("moving_avg", &sensor);
    sensors.addSensor("median", &sensor);
    sensors.addSensor("kalman", &sensor);
    sensors.addSensor("exponential", &sensor);
    sensors.init();
    
    // Attach different filters
    FilterParams params;
    
    // Moving average
    params.movingAverage.windowSize = 10;
    sensors.attachFilter("moving_avg", "volt", FILTER_MOVING_AVERAGE, params);
    
    // Median
    params.median.windowSize = 9;
    sensors.attachFilter("median", "volt", FILTER_MEDIAN, params);
    
    // Kalman
    params.kalman.processNoise = 0.01f;
    params.kalman.measurementNoise = 0.1f;
    params.kalman.estimateError = 0.1f;
    sensors.attachFilter("kalman", "volt", FILTER_KALMAN, params);
    
    // Exponential
    params.exponential.alpha = 0.2f;
    sensors.attachFilter("exponential", "volt", FILTER_EXPONENTIAL, params);
}

void loop() {
    sensors.update();
    
    if (sensors.isUpdated("raw")) {
        float raw = sensors.getValue<float>("raw", "volt");
        float movAvg = sensors.getFilteredValue("moving_avg", "volt");
        float median = sensors.getFilteredValue("median", "volt");
        float kalman = sensors.getFilteredValue("kalman", "volt");
        float exponential = sensors.getFilteredValue("exponential", "volt");
        
        Serial.print("Raw: ");
        Serial.print(raw, 4);
        Serial.print(" | MovAvg: ");
        Serial.print(movAvg, 4);
        Serial.print(" | Median: ");
        Serial.print(median, 4);
        Serial.print(" | Kalman: ");
        Serial.print(kalman, 4);
        Serial.print(" | Exp: ");
        Serial.println(exponential, 4);
    }
    
    delay(100);
}

Real-World Applications

Temperature Monitoring

DHTSensV2 dht(2, DHT22);
sensors.addSensor("climate", &dht);

// Kalman filter for smooth temperature readings
FilterParams tempFilter;
tempFilter.kalman.processNoise = 0.001f;     // Temperature changes slowly
tempFilter.kalman.measurementNoise = 0.5f;   // DHT22 accuracy
tempFilter.kalman.estimateError = 1.0f;
sensors.attachFilter("climate", "temperature", FILTER_KALMAN, tempFilter);

// Moving average for humidity (less critical)
FilterParams humFilter;
humFilter.movingAverage.windowSize = 5;
sensors.attachFilter("climate", "humidity", FILTER_MOVING_AVERAGE, humFilter);

Distance Measurement

UltrasonicSensV2 ultrasonic(9, 10);
sensors.addSensor("distance", &ultrasonic);

// Median filter to remove echo spikes
FilterParams distFilter;
distFilter.median.windowSize = 7;
sensors.attachFilter("distance", "distance_cm", FILTER_MEDIAN, distFilter);

Gas Detection

MQSensV2 gasSensor("ESP32", 3.3, 12, A0, "MQ-135");
sensors.addSensor("air", &gasSensor);

// Exponential filter for gas concentration
FilterParams gasFilter;
gasFilter.exponential.alpha = 0.15f;  // Slow response for stability
sensors.attachFilter("air", "ppm", FILTER_EXPONENTIAL, gasFilter);

Power Monitoring

INA219SensV2 power;
sensors.addSensor("power", &power);

// Moving average for current (fast-changing)
FilterParams currentFilter;
currentFilter.movingAverage.windowSize = 20;
sensors.attachFilter("power", "current", FILTER_MOVING_AVERAGE, currentFilter);

// Exponential for voltage (relatively stable)
FilterParams voltageFilter;
voltageFilter.exponential.alpha = 0.3f;
sensors.attachFilter("power", "voltage", FILTER_EXPONENTIAL, voltageFilter);

Environmental Station

BME680SensV2 environment;
sensors.addSensor("env", &environment);

// Kalman for temperature
FilterParams tempParams;
tempParams.kalman.processNoise = 0.001f;
tempParams.kalman.measurementNoise = 0.3f;
tempParams.kalman.estimateError = 0.5f;
sensors.attachFilter("env", "temperature", FILTER_KALMAN, tempParams);

// Moving average for humidity
FilterParams humParams;
humParams.movingAverage.windowSize = 10;
sensors.attachFilter("env", "humidity", FILTER_MOVING_AVERAGE, humParams);

// Exponential for pressure (slow-changing)
FilterParams pressParams;
pressParams.exponential.alpha = 0.1f;
sensors.attachFilter("env", "pressure", FILTER_EXPONENTIAL, pressParams);

// Median for gas (remove spikes)
FilterParams gasParams;
gasParams.median.windowSize = 5;
sensors.attachFilter("env", "gas", FILTER_MEDIAN, gasParams);

Filter Selection Guide

Signal CharacteristicRecommended FilterSettings
Random Gaussian noiseMoving AveragewindowSize: 10-20
Occasional spikesMedianwindowSize: 5-9
Smooth signal, optimal filteringKalmanTune Q, R based on signal
Fast-changing with noiseExponentialalpha: 0.3-0.5
Slow-changing, very noisyKalman or Moving AverageLarge window or low Q
Mixed noise + spikesMedian + ExponentialChain filters

Advanced Techniques

Cascaded Filters

Apply multiple filters in sequence (requires manual implementation):
// First pass: Remove spikes with median
FilterParams medianParams;
medianParams.median.windowSize = 5;
sensors.attachFilter("sensor", "value", FILTER_MEDIAN, medianParams);
float stage1 = sensors.getFilteredValue("sensor", "value");

// Second pass: Smooth with exponential (manual)
static float smoothed = 0;
smoothed = 0.2f * stage1 + 0.8f * smoothed;

Adaptive Filtering

Adjust filter parameters based on signal conditions:
void adaptiveFilter() {
    float variance = calculateVariance();  // Implement variance calculation
    
    FilterParams params;
    if (variance > 10.0) {
        // High noise: increase smoothing
        params.movingAverage.windowSize = 20;
    } else {
        // Low noise: faster response
        params.movingAverage.windowSize = 5;
    }
    
    sensors.updateFilter("sensor", "value", FILTER_MOVING_AVERAGE, params);
}

Filter Warmup

Pre-fill filter buffers for faster convergence:
void warmupFilter() {
    sensors.resetFilter("temp", "temperature");
    
    // Take initial readings to fill buffer
    for (int i = 0; i < 20; i++) {
        sensors.update();
        delay(50);
    }
    
    // Filter is now warmed up
    float stable = sensors.getFilteredValue("temp", "temperature");
}

Performance Considerations

Memory Usage

Filter TypeMemory per Value
Moving AveragewindowSize * 4 bytes
MedianwindowSize * 8 bytes (2 buffers)
Kalman~20 bytes (state variables)
Exponential~4 bytes (last value)

CPU Usage

Filter TypeRelative CPU Cost
Exponential1x (lowest)
Moving Average2x
Kalman3x
Median5x (sorting)

Best Practices

Start Simple: Begin with moving average or exponential filters. Only use Kalman if you need optimal filtering and can tune the parameters properly.
Don’t Over-Filter: Excessive filtering introduces lag. Match the filter strength to your application’s response time requirements.
Combine with Calibration: Apply calibration first, then filtering:
float raw = calibrator.getRawValue("temp", "temperature");
float calibrated = calibrator.getCalibratedValue("temp", "temperature");
float filtered = sensors.getFilteredValue("temp", "temperature");

Troubleshooting

Filter Not Working

// Verify filter is attached
if (!sensors.hasFilter("temp", "temperature")) {
    Serial.println("Filter not attached!");
    // Reattach filter
}

// Verify filter system is enabled
if (!sensors.hasFilterSystem()) {
    Serial.println("Filter system not initialized!");
}

Too Much Lag

// Reduce window size
params.movingAverage.windowSize = 5;  // Was 20
sensors.updateFilter("sensor", "value", FILTER_MOVING_AVERAGE, params);

// Or increase alpha
params.exponential.alpha = 0.4f;  // Was 0.1
sensors.updateFilter("sensor", "value", FILTER_EXPONENTIAL, params);

Not Enough Smoothing

// Increase window size
params.movingAverage.windowSize = 20;  // Was 5

// Or decrease alpha
params.exponential.alpha = 0.1f;  // Was 0.3

// Or switch to Kalman
params.kalman.processNoise = 0.001f;
params.kalman.measurementNoise = 1.0f;
params.kalman.estimateError = 1.0f;
sensors.updateFilter("sensor", "value", FILTER_KALMAN, params);

Next Steps

Alerts

Set alerts on filtered sensor values

Calibration

Combine filtering with calibration

Build docs developers (and LLMs) love