You Want Electronic Components for your project. 

For more information, contact us at: 

Integrate Sensors to Collect Environmental and System Data



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?

I'm an Embedded & AIoT Systems Engineer and the Founder & CEO of Nyereka Tech, a Rwanda-based startup enabling access to STEM tools, training, and innovation. I'm passionate about empowering young innovators through practical technology education and local manufacturing solutions.