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
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.01 f ; // How much the signal changes
params . kalman . measurementNoise = 0.1 f ; // Sensor measurement noise
params . kalman . estimateError = 0.1 f ; // 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.001 f ; // Temperature changes slowly
tempFilter . kalman . measurementNoise = 0.5 f ; // DHT22 ±0.5°C accuracy
tempFilter . kalman . estimateError = 1.0 f ;
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:
Start with R = sensor datasheet accuracy
Set Q = R / 10 for slow signals, Q = R for fast signals
Set P = R
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.1 f ; // 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.2 f ; // 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.005 f ;
newParams . kalman . measurementNoise = 0.2 f ;
newParams . kalman . estimateError = 0.1 f ;
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.01 f ;
params . kalman . measurementNoise = 0.1 f ;
params . kalman . estimateError = 0.1 f ;
sensors . attachFilter ( "kalman" , "volt" , FILTER_KALMAN, params);
// Exponential
params . exponential . alpha = 0.2 f ;
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.001 f ; // Temperature changes slowly
tempFilter . kalman . measurementNoise = 0.5 f ; // DHT22 accuracy
tempFilter . kalman . estimateError = 1.0 f ;
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.15 f ; // 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.3 f ;
sensors . attachFilter ( "power" , "voltage" , FILTER_EXPONENTIAL, voltageFilter);
Environmental Station
BME680SensV2 environment;
sensors . addSensor ( "env" , & environment);
// Kalman for temperature
FilterParams tempParams;
tempParams . kalman . processNoise = 0.001 f ;
tempParams . kalman . measurementNoise = 0.3 f ;
tempParams . kalman . estimateError = 0.5 f ;
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.1 f ;
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 Characteristic Recommended Filter Settings Random Gaussian noise Moving Average windowSize: 10-20 Occasional spikes Median windowSize: 5-9 Smooth signal, optimal filtering Kalman Tune Q, R based on signal Fast-changing with noise Exponential alpha: 0.3-0.5 Slow-changing, very noisy Kalman or Moving Average Large window or low Q Mixed noise + spikes Median + Exponential Chain 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.2 f * stage1 + 0.8 f * 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" );
}
Memory Usage
Filter Type Memory per Value Moving Average windowSize * 4 bytes Median windowSize * 8 bytes (2 buffers) Kalman ~20 bytes (state variables) Exponential ~4 bytes (last value)
CPU Usage
Filter Type Relative CPU Cost Exponential 1x (lowest) Moving Average 2x Kalman 3x Median 5x (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.4 f ; // 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.1 f ; // Was 0.3
// Or switch to Kalman
params . kalman . processNoise = 0.001 f ;
params . kalman . measurementNoise = 1.0 f ;
params . kalman . estimateError = 1.0 f ;
sensors . updateFilter ( "sensor" , "value" , FILTER_KALMAN, params);
Next Steps
Alerts Set alerts on filtered sensor values
Calibration Combine filtering with calibration