Sensor fusion is the technique of combining readings from several sensors so the result is more accurate, more stable or more complete than any single sensor could provide. For IoT developers it is often the difference between a jittery, drifting signal and one you can trust in a dashboard or a model. This article explains the main fusion approaches in plain terms, shows when to fuse at the edge versus in the cloud, and gives you working ESP32 and Python code that publishes fused values over MQTT.
Why Combine Sensors at All
Every sensor has weaknesses. An accelerometer measures tilt well when the device is still but is thrown off by every vibration. A gyroscope measures rotation smoothly but its integrated angle drifts over time. GPS gives absolute position but updates slowly and jumps around under trees; wheel odometry is smooth but accumulates error. Two cheap temperature probes in the same tank disagree slightly and each has occasional spikes.
Fusion exploits the fact that these weaknesses are different. If one sensor is noisy at high frequencies and another drifts at low frequencies, combining them gives a signal that is good across the board. If two sensors measure the same thing with independent noise, a weighted combination is more precise than either alone. And when one sensor fails, a fused estimate can detect the inconsistency instead of silently reporting a wrong value.
Three Levels of Sensor Fusion
| Level | What is combined | Example | Typical method |
|---|---|---|---|
| Low level (data) | Raw measurements of the same quantity | Two temperature probes, accelerometer plus gyroscope | Weighted average, complementary filter, Kalman filter |
| Feature level | Features derived from different sensors | Vibration RMS plus motor current plus temperature | Feature vectors for an ML model |
| Decision level | Outputs of separate detectors | Camera says "person", PIR says "motion" | Voting, rules, Bayesian combination |
Low-level fusion is the focus of this article because it is where filters such as complementary and Kalman filters live. Feature-level fusion is the everyday input of machine learning models, as in predictive maintenance machine learning, where vibration, temperature and current together reveal degradation that none shows clearly alone.
The Complementary Filter
The complementary filter is the simplest useful fusion method and a good starting point for inertial sensors. To estimate a tilt angle, it integrates the gyroscope rate for short-term changes and gently pulls the result toward the accelerometer angle to remove long-term drift:
angle = alpha * (angle + gyro_rate * dt) + (1 - alpha) * accel_angle
With alpha around 0.98 and a sample period of 10 ms, the gyroscope dominates over fractions of a second and the accelerometer dominates over a few seconds. The filter needs no matrices, runs in microseconds on a microcontroller and is easy to tune: increase alpha if the output is noisy, decrease it if the output drifts.
The Kalman Filter
The Kalman filter generalises the same idea. It keeps an estimate of the state and of its own uncertainty, predicts how the state will change, then corrects the prediction with each measurement, weighting the correction by how much it trusts the measurement relative to the prediction. When the measurement is noisy, the filter leans on the prediction; when the prediction is uncertain, it leans on the measurement.
For a single quantity with two sensors, the Kalman update is short enough to read in full:
import json
import random
import time
import uuid
import paho.mqtt.client as mqtt
class Kalman1D:
def __init__(self, x0, p0=1.0, q=0.001):
self.x, self.p, self.q = x0, p0, q # estimate, its variance, process noise
def predict(self):
self.p += self.q
def update(self, z, r):
k = self.p / (self.p + r) # Kalman gain
self.x += k * (z - self.x)
self.p *= (1 - k)
return self.x
BASE = f"connectmq/demo/{uuid.uuid4().hex[:8]}/tank1"
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="fusion-" + uuid.uuid4().hex[:8])
client.connect("broker.connectmq.com", 1883, keepalive=60)
client.loop_start()
kf = Kalman1D(x0=20.0)
true_temp = 20.0
R_PT100, R_NTC = 0.05 ** 2, 0.4 ** 2 # measurement variances from datasheets or tests
for _ in range(300):
true_temp += random.gauss(0, 0.02)
pt100 = true_temp + random.gauss(0, 0.05)
ntc = true_temp + random.gauss(0, 0.4) + (5 if random.random() < 0.02 else 0) # occasional spike
kf.predict()
if abs(ntc - kf.x) < 3 * (R_NTC + kf.p) ** 0.5: # gate out implausible readings
kf.update(ntc, R_NTC)
fused = kf.update(pt100, R_PT100)
payload = {"ts": int(time.time() * 1000), "value": round(fused, 3), "variance": round(kf.p, 6),
"raw": {"pt100": round(pt100, 3), "ntc": round(ntc, 3)}}
client.publish(f"{BASE}/temperature/fused", json.dumps(payload))
time.sleep(1)
client.loop_stop()
client.disconnect()
Install with pip install paho-mqtt and subscribe with mosquitto_sub -h broker.connectmq.com -p 1883 -i fusion-watch-$RANDOM -t "connectmq/demo/+/tank1/#" -v to see the fused value next to the raw readings. Notice the gate: readings that disagree with the current estimate by more than three standard deviations are skipped, which is how the filter rejects the NTC spikes. Publishing the variance alongside the value lets consumers know how confident the estimate is.
For multi-dimensional problems such as position and velocity from GPS and an IMU, the same structure uses matrices, and nonlinear systems use the extended or unscented Kalman filter. Libraries such as FilterPy in Python or the Madgwick and Mahony filters for orientation save you from implementing those by hand.
Fusing at the Edge: ESP32 Example
Inertial fusion must run close to the sensor because it needs fast, regularly spaced samples, often 100 Hz or more. Publishing every raw sample over MQTT would waste bandwidth and battery and add jitter. The better pattern is to fuse on the device and publish the result at a modest rate.
This sketch reads an MPU6050 over I2C on an ESP32, runs a complementary filter at 100 Hz and publishes the fused pitch angle twice per second to broker.connectmq.com. It uses the Adafruit MPU6050 and PubSubClient libraries.
#include <WiFi.h>
#include <PubSubClient.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Wire.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* BROKER = "broker.connectmq.com";
const char* TOPIC = "connectmq/demo/imu-7f3a/arm1/pitch";
WiFiClient net;
PubSubClient mqtt(net);
Adafruit_MPU6050 mpu;
float pitch = 0.0f;
const float ALPHA = 0.98f;
unsigned long lastSample = 0, lastPublish = 0;
void connectMqtt() {
while (!mqtt.connected()) {
String id = "esp32-imu-" + String((uint32_t)ESP.getEfuseMac(), HEX);
if (!mqtt.connect(id.c_str())) delay(2000);
}
}
void setup() {
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(500);
mqtt.setServer(BROKER, 1883);
if (!mpu.begin()) {
Serial.println("MPU6050 not found");
while (true) delay(1000);
}
mpu.setGyroRange(MPU6050_RANGE_250_DEG);
mpu.setFilterBandwidth(MPU6050_BAND_44_HZ);
lastSample = micros();
}
void loop() {
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
if (micros() - lastSample >= 10000) { // 100 Hz
float dt = (micros() - lastSample) / 1e6f;
lastSample = micros();
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
float accelPitch = atan2f(-a.acceleration.x,
sqrtf(a.acceleration.y * a.acceleration.y + a.acceleration.z * a.acceleration.z)) * 180.0f / PI;
float gyroRate = g.gyro.y * 180.0f / PI; // rad/s to deg/s
pitch = ALPHA * (pitch + gyroRate * dt) + (1.0f - ALPHA) * accelPitch;
}
if (millis() - lastPublish >= 500) { // 2 messages per second
lastPublish = millis();
char payload[64];
snprintf(payload, sizeof(payload), "{\"pitch\":%.2f,\"unit\":\"deg\"}", pitch);
mqtt.publish(TOPIC, payload);
}
}
Change the topic segment imu-7f3a to something unique. For a production device, switch to WiFiClientSecure on port 8883 with your ConnectMQ username, password and client ID; the ESP32 MQTT tutorial covers the TLS setup in detail.
Edge or Cloud Fusion: How to Decide
- Fuse at the edge when sensors are on the same device, sample rates are high, latency matters (balancing, stabilisation, safety), or bandwidth and battery are limited.
- Fuse in the cloud or on a server when sensors are on different devices, rates are low, you need historical context, or you want to change the algorithm without a firmware update.
- Publish both for a while during development: raw values at a reduced rate and fused values, so you can verify the filter against reality.
- Always timestamp at the source when fusing across devices, because broker arrival order is not measurement order.
This is the same trade-off discussed in what edge AI is: computation moves to where the data and latency requirements are.
Common Sensor Fusion Pitfalls
- Wrong noise parameters. A Kalman filter is only as good as its assumed measurement and process noise. Measure noise from a stationary recording rather than guessing.
- Mismatched axes and units. Gyroscopes often report rad/s; accelerometer axes may be mounted differently than you assume. Check signs by tilting the board by hand.
- Uneven sample timing. Filters assume a known
dt. Compute it from timestamps rather than assuming a constant. - Unsynchronised sources. Fusing two devices whose clocks differ by seconds produces artefacts. Synchronise with NTP and carry source timestamps.
- Hiding sensor faults. Fusion can mask a failing sensor. Publish raw health indicators or innovation values so faults remain visible, which also feeds IoT anomaly detection.
Where Fused Data Goes Next
Fused values are better inputs for dashboards, alerts and models. ConnectMQ acts as the hosted MQTT broker that carries them from devices to consumers: the live topic explorer shows fused and raw topics side by side, and threshold alert rules can fire on a fused value, such as a tilt angle beyond a safe limit. For how models and connected devices work together more broadly, see the AI and IoT guide.
Get Started with ConnectMQ
Try the examples against the public MQTT test broker first, then create a free Serverless broker at app.connectmq.com, no credit card required, to get private credentials and TLS for your devices.
Frequently Asked Questions
What is sensor fusion in IoT?
Sensor fusion combines measurements from multiple sensors to produce an estimate that is more accurate, stable or complete than any single sensor, for example a drift-free tilt angle from an accelerometer and a gyroscope.
Should I use a complementary filter or a Kalman filter?
Start with a complementary filter for simple orientation problems on microcontrollers. Use a Kalman filter when you need to combine sensors with known noise characteristics, estimate several related states or report uncertainty.
Should sensor fusion run on the device or in the cloud?
Run it on the device when sensors are local and fast, because sending every raw sample is expensive and adds jitter. Run it centrally when sensors are on different devices or the update rate is low.
Can I still access raw data if I fuse at the edge?
Yes. A common pattern is to publish fused values continuously and raw values at a lower rate or on demand on separate topics, which keeps bandwidth low while allowing verification.



