Lesson 4.2 — Integrate Sensors to Collect Environmental and System Data
This lesson moves from knowing about sensors to working with them. You will wire up, calibrate, and program five commonly used IoT sensors, learning to handle noise, invalid readings, and multi-sensor integration.
Sensor Integration Principles
| Principle |
Explanation |
| Check voltage compatibility |
Confirm the sensor’s VCC range. 5 V sensors connected to 3.3 V ESP32 signals WILL cause unreliable readings or damage. Use voltage dividers or level shifters on signal lines. |
| Read the datasheet |
Every sensor has a datasheet specifying initialisation time, minimum sampling interval, accuracy, and calibration requirements. DHT22 needs 2 seconds between readings; MQ-2 needs 20–48 hours of burn-in. |
| Validate every reading |
Test for out-of-range values, NaN, and stale data before using any sensor reading in logic or transmitting to the cloud. A NaN temperature reading that opens a greenhouse vent causes real-world damage. |
| Average noisy readings |
Analog sensors are noisy. Take 5–10 readings and average them. For fast-changing signals, use a rolling average (sliding window) to smooth without adding latency. |
| Use non-blocking timing |
Never delay() between sensor reads. Use millis()-based timing so other tasks continue running while waiting for the sensor’s required interval. |
Sensor 1: HC-SR04 — Water Tank Level Monitor
| Pin |
Connection |
Notes |
| VCC |
5 V |
Power |
| GND |
GND |
Ground |
| TRIG |
Arduino pin 9 |
Send 10 μs trigger pulse |
| ECHO |
Arduino pin 10 |
Use voltage divider (1kΩ + 2kΩ) if ESP32. Receive echo pulse. |
C/C++ — Water Tank Level Monitor (HC-SR04)
#define TRIG_PIN 9
#define ECHO_PIN 10
#define TANK_HEIGHT_CM 100 // Full tank depth in cm
#define SENSOR_OFFSET_CM 5 // Distance from sensor to full water surface
void setup() {
Serial.begin(9600);
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
}
long measureDistanceCm() {
// Trigger: send 10 microsecond HIGH pulse
digitalWrite(TRIG_PIN, LOW); delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH); delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
// Echo: measure duration of return pulse
long duration = pulseIn(ECHO_PIN, HIGH, 30000); // 30ms timeout
if (duration == 0) return -1; // Timeout = no echo = error
return duration * 0.0343 / 2; // Convert to cm
}
void loop() {
long distanceCm = measureDistanceCm();
if (distanceCm < 0 || distanceCm > TANK_HEIGHT_CM) {
Serial.println("ERROR: Sensor out of range");
} else {
long waterLevelCm = TANK_HEIGHT_CM - distanceCm - SENSOR_OFFSET_CM;
int levelPercent = constrain(map(waterLevelCm, 0, TANK_HEIGHT_CM, 0, 100), 0, 100);
Serial.print("Distance: "); Serial.print(distanceCm); Serial.print(" cm ");
Serial.print("Level: "); Serial.print(levelPercent); Serial.println("%");
if (levelPercent < 20) Serial.println("WARNING: Tank low — activate pump!");
}
delay(500);
}
KEY
constrain(value, min, max) clips a value to stay within bounds. map() rescales a value from one range to another. Used together, they safely convert a raw distance into a bounded 0–100% level reading.
Sensor 2: PIR Motion Sensor — Occupancy-Based Lighting
C/C++ — PIR Occupancy-Based LED Control
#define PIR_PIN 5
#define LED_PIN 6
bool lightOn = false;
unsigned long lastMotion = 0;
const long AUTO_OFF_DELAY = 30000; // 30s after last motion
void setup() {
pinMode(PIR_PIN, INPUT);
pinMode(LED_PIN, OUTPUT);
Serial.begin(9600);
Serial.println("Waiting for PIR warm-up (60 sec)...");
delay(60000); // PIR warm-up: only delay() here is acceptable
Serial.println("PIR ready.");
}
void loop() {
int motionDetected = digitalRead(PIR_PIN);
if (motionDetected == HIGH) {
lastMotion = millis();
if (!lightOn) {
lightOn = true;
digitalWrite(LED_PIN, HIGH);
Serial.println("Motion detected — light ON");
}
}
// Auto-off after no motion for AUTO_OFF_DELAY ms
if (lightOn && (millis() - lastMotion > AUTO_OFF_DELAY)) {
lightOn = false;
digitalWrite(LED_PIN, LOW);
Serial.println("No motion — light OFF");
}
}
NOTE
PIR sensors have two trimpots on the back: one adjusts sensitivity (detection range), the other adjusts hold time. Set hold time to minimum when using software-based timers like AUTO_OFF_DELAY above.
Sensor 3: MQ-2 Gas Sensor — Smoke and Gas Alarm
C/C++ — MQ-2 Gas and Smoke Alarm
#define MQ2_AO_PIN A0 // Analog output
#define MQ2_DO_PIN 7 // Digital output (threshold-based)
#define BUZZER_PIN 8
#define LED_RED_PIN 9
const int ANALOG_THRESHOLD = 400; // Adjust based on calibration
void setup() {
Serial.begin(9600);
pinMode(MQ2_DO_PIN, INPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(LED_RED_PIN, OUTPUT);
Serial.println("MQ-2 warming up (2 min)...");
delay(120000); // 2-minute warm-up after power-on
Serial.println("MQ-2 ready.");
}
void loop() {
int analogValue = analogRead(MQ2_AO_PIN);
int digitalAlert = digitalRead(MQ2_DO_PIN); // LOW = threshold exceeded
Serial.print("Gas level: "); Serial.print(analogValue);
Serial.print(" | Alert: "); Serial.println(digitalAlert == LOW ? "YES" : "no");
bool alarm = (analogValue > ANALOG_THRESHOLD) || (digitalAlert == LOW);
if (alarm) {
tone(BUZZER_PIN, 2000); // 2 kHz alarm tone
digitalWrite(LED_RED_PIN, HIGH);
Serial.println("!!! GAS/SMOKE DETECTED !!!");
} else {
noTone(BUZZER_PIN);
digitalWrite(LED_RED_PIN, LOW);
}
delay(200);
}
SAFETY
The MQ-2 sensor element operates at ~300°C during normal use. The top of the sensor cap gets HOT. Do not touch it during operation. Ensure adequate ventilation around the sensor in enclosures.
Multi-Sensor Integration — Environmental Station (DHT22 + LDR + PIR)
C/C++ — Multi-Sensor Non-Blocking Data Logger
#include <DHT.h>
#define DHT_PIN 4
#define LDR_PIN A0
#define PIR_PIN 5
DHT dht(DHT_PIN, DHT22);
unsigned long lastDHT = 0, lastLDR = 0, lastLog = 0;
const long DHT_INTERVAL = 3000; // DHT22: every 3 seconds
const long LDR_INTERVAL = 500; // LDR: every 500 ms
const long LOG_INTERVAL = 10000; // Summary: every 10 seconds
float temp = 0, hum = 0;
int ldrSmooth = 0;
bool motionState = false;
void setup() {
Serial.begin(9600);
dht.begin();
pinMode(PIR_PIN, INPUT);
Serial.println("Environmental station starting...");
}
void loop() {
unsigned long now = millis();
// Task 1: Read DHT22 every 3 seconds
if (now - lastDHT >= DHT_INTERVAL) {
lastDHT = now;
float t = dht.readTemperature();
float h = dht.readHumidity();
if (!isnan(t) && !isnan(h)) { temp = t; hum = h; }
}
// Task 2: Smooth LDR every 500 ms
if (now - lastLDR >= LDR_INTERVAL) {
lastLDR = now;
ldrSmooth = (ldrSmooth * 3 + analogRead(LDR_PIN)) / 4;
}
// Task 3: PIR is event-driven — read every loop
motionState = (digitalRead(PIR_PIN) == HIGH);
// Task 4: Log summary every 10 seconds
if (now - lastLog >= LOG_INTERVAL) {
lastLog = now;
Serial.print("T:"); Serial.print(temp, 1);
Serial.print("C H:"); Serial.print(hum, 1);
Serial.print("% Light:"); Serial.print(ldrSmooth);
Serial.print(" Motion:"); Serial.println(motionState ? "YES" : "no");
}
}
TRY
Add a BMP280 (I2C barometric pressure) as a 5th sensor. Read it every 5 seconds using a 5th millis() timer. Include pressure in hPa in the log summary line.
Sensor Calibration and Signal Quality
1. Offset Calibration
C/C++ — Offset Calibration
// DHT22 reads 1.5 degrees high compared to calibrated reference
const float TEMP_OFFSET = -1.5;
float getCalibratedTemp() {
return dht.readTemperature() + TEMP_OFFSET;
}
2. Rolling Average Filter
C/C++ — Rolling Average for Noise Reduction
const int WINDOW = 8;
int buffer[WINDOW] = {0};
int idx = 0;
long sum = 0;
int smoothedRead(int pin) {
sum -= buffer[idx];
buffer[idx] = analogRead(pin);
sum += buffer[idx];
idx = (idx + 1) % WINDOW;
return sum / WINDOW;
}
3. Threshold Hysteresis
C/C++ — Hysteresis to Prevent Chattering
// Without hysteresis: chatters if soil oscillates around single threshold
// With hysteresis: turn pump ON at < 350, turn pump OFF at > 450
const int PUMP_ON_THRESHOLD = 350;
const int PUMP_OFF_THRESHOLD = 450;
bool pumpState = false;
void updatePump(int soilMoisture) {
if (!pumpState && soilMoisture < PUMP_ON_THRESHOLD) {
pumpState = true;
digitalWrite(PUMP_RELAY_PIN, HIGH);
} else if (pumpState && soilMoisture > PUMP_OFF_THRESHOLD) {
pumpState = false;
digitalWrite(PUMP_RELAY_PIN, LOW);
}
// If between thresholds: maintain current state (hysteresis gap)
}
Check Your Understanding — Lesson 4.2
?
1. A water tank level sensor (HC-SR04) occasionally returns -1. What does this indicate, and what two conditions in measureDistanceCm() handle this?
?
2. Explain what ‘chattering’ means in sensor threshold logic and describe how hysteresis resolves it. Give specific ON and OFF threshold values for a soil moisture pump controller.
?
3. In the multi-sensor data logger, why is the PIR sensor read on every loop() iteration (event-driven) while the DHT22 and LDR are read on timers? What would go wrong if the PIR were also on a 3-second timer?