40  Lab: Sensor Best Practices

sensors
sensor
best
practices
Start With the Measurement Story

A good sensor lab leaves behind evidence, not just a working demo. Start by recording the setup, expected range, calibration point, noise behavior, failure cases, and the exact test that would catch a bad reading.

40.1 In 60 Seconds

Building reliable sensor systems requires more than just wiring – you need data validation, noise filtering, error handling, and health monitoring. Always check for NaN values, apply moving average filters to smooth noisy readings, use sensor fusion to combine multiple sensors for better accuracy, and implement hysteresis in threshold-based controls to prevent rapid switching.

Key Concepts
  • Breadboard Power Rails: The two horizontal rails on a breadboard carry power and ground; always verify the rail is connected to your supply before troubleshooting any circuit behavior
  • Multimeter Continuity Test: A mode that beeps when two probed points are electrically connected; fastest way to verify wiring, detect broken connections, and confirm ground paths
  • Datasheet Pinout Verification: Component pinouts are perspective-dependent (top vs. bottom of chip). Always cross-reference the datasheet before connecting power to avoid reversed VCC/GND connections
  • Incremental Testing: Testing each sub-circuit section independently before combining; adding one component at a time and verifying correct operation reduces debugging scope to the last thing added
  • Serial Monitor Debugging: Using UART print statements to stream sensor readings and intermediate values to a PC terminal; the most accessible debugging tool for embedded firmware
  • Power-On Sequencing: Some sensor modules require VCC to stabilize before the host begins I2C/SPI communication; add a startup delay (typically 10-100 ms) after power-on to allow the sensor’s internal controller to initialize
  • Decoupling Capacitor Placement: Place 100 nF ceramic capacitors within 10 mm of every sensor’s VCC pin to filter power supply noise; missing decoupling is the most common cause of intermittent analog reading noise
  • ESD Handling: Electrostatic discharge from handling bare PCBs can permanently damage CMOS circuits; touch a grounded metal surface before handling bare boards or use an ESD wrist strap in dry environments

40.2 Learning Objectives

By the end of this chapter, you will be able to:

  • Implement sensor best practices: Integrate validation, filtering, and error handling into production code
  • Construct robust data pipelines: Design multi-stage processing chains from raw sensor data to storage
  • Execute hands-on labs: Interface DHT22, perform sensor fusion, calibrate sensors, and deploy health monitoring
  • Diagnose common issues: Identify and resolve timing errors, wiring problems, and library conflicts
  • Validate and verify: Create reproducible sensor implementations with structured testing procedures
For Beginners: Sensor Best Practices

Building a reliable sensor system is like building a house – having good materials (sensors) is not enough; you also need good construction practices. This chapter covers essential habits like always checking if a reading makes sense before using it (is -500 degrees Celsius realistic?), smoothing out noisy data with simple averaging, and having a backup plan when a sensor stops working.

40.3 Introduction

A sensor that works perfectly on your bench may fail unpredictably in the field. Temperature swings, moisture, vibration, and power fluctuations all conspire to degrade readings over time. This chapter equips you with the defensive programming patterns and diagnostic techniques that distinguish a prototype from a production-quality sensor system. Through five hands-on labs, you will build real implementations of data filtering, multi-sensor fusion, two-point calibration, automated health monitoring, and hysteresis-based relay control – the essential toolkit for any IoT deployment.

Chapter Roadmap

This chapter turns “the sensor reads a number” into a repeatable measurement workflow:

  1. First you name the defensive habits: validation, timeouts, calibration, filtering, failure handling, power, protection, and placement.
  2. Then you quantify the tradeoffs behind filtering, fusion, and hysteresis so every threshold has a reason.
  3. Next you practice those ideas in labs for DHT22 filtering, DS18B20 fusion, two-point calibration, health checks, and relay control.
  4. After that you use quizzes and fault examples to connect each code pattern to the failure it catches.
  5. Finally you decide when this page is enough and when the companion noise-and-averaging chapter should take over.

Checkpoints recap the production rule you have just earned, and “Optional ESP32 Implementation” blocks can be skimmed on a first read.

40.4 Prerequisites

Required Knowledge:

Hardware Requirements:

  • ESP32 development board
  • DHT22 temperature/humidity sensor
  • DS18B20 temperature sensors (3x for fusion lab)
  • Breadboard, jumper wires, resistors

40.5 Best Practices

Time: ~15 min | Level: Foundational | Code: P06.C10.U04

Sensor Implementation Tips
  1. Always validate sensor readings: Check for NaN, out-of-range values
  2. Implement timeouts: Don’t let sensor reads block indefinitely
  3. Calibrate regularly: Environmental drift affects accuracy over time
  4. Filter noisy data: Use moving average or Kalman filters
  5. Handle sensor failures gracefully: Implement retry logic and fallbacks
  6. Consider power consumption: Use deep sleep between readings for battery applications
  7. Protect sensors: Use appropriate enclosures for harsh environments
  8. Document sensor placement: Location affects readings (heat sources, airflow, etc.)
Multi-sensor data acquisition pipeline showing sensor array, validation, filtering, calibration, and data output stages with error handling at every step
Figure 40.1: Multi-Sensor Data Acquisition Pipeline with Validation and Error Handling
Common Pitfalls
  • Not waiting for sensor stabilization: DHT22 needs 2 seconds between reads
  • Wrong voltage levels: 5V sensor on 3.3V GPIO can damage microcontroller
  • Poor grounding: Causes noise and erratic readings
  • Ignoring measurement errors: Always check for failed reads
  • Over-sampling: Wastes power without improving accuracy
  • Incorrect pull-up resistors: I2C and 1-Wire need proper resistors

Phoebe the physics guide

Phoebe’s Why

“Over-sampling wastes power” and “wait 2 seconds between DHT22 reads” look like two unrelated tips, but they are the same physics from opposite sides. Any sensor reading is only as fast as the real-world signal it needs to track: sample slower than twice the fastest rate that signal actually moves, and a genuine fast wiggle does not just get missed, it gets folded back and relabeled as a false, slower drift. Sample much faster than that, and every extra reading burns power for zero new information, because there was nothing left to resolve. The DHT22’s fixed 2-second floor is a hardware limit that happens to sit nowhere near a real garden’s thermal timescale, which is exactly why “Sammy” later in this chapter can be smoothed with a smoothing average instead of frightened by a Nyquist violation. The soil-moisture ADC in the health-monitoring lab hides a second, quieter limit: even a perfect 12-bit reading rounds the true moisture level to the nearest of 4,096 steps, so a little noise is built into every count before anything ever goes wrong.

The Derivation

Alias-free sampling requires the sample rate to clear twice the fastest real content:

\[f_s \geq 2f_{max} \quad\Longrightarrow\quad f_{max} = \frac{f_s}{2} = \frac{1}{2T}\]

A step size \(q\) over an \(N\)-bit range rounds every sample to the nearest code; the rounding error is uniform on \([-q/2,+q/2]\), giving RMS noise:

\[q = \frac{\text{full scale}}{2^N}, \qquad \sigma_q = \frac{q}{\sqrt{12}}\]

Comparing the largest representable signal to that noise floor gives the ideal converter SNR:

\[\mathrm{SNR} \approx 6.02N + 1.76\ \text{dB}\]

Worked Numbers: This Chapter’s Own DHT22 Interval and 12-Bit Soil ADC

  • DHT22 Nyquist limit: this chapter’s own \(T=2\) s minimum interval gives \(f_s = 1/2 = 0.500\) Hz, so \(f_{max} = 0.250\) Hz – alias-free only for garden temperature content that completes a full swing no faster than every \(4.00\) s. A real enclosure’s thermal mass makes that an easy limit to clear, which is why the “Over-sampling wastes power” pitfall, not aliasing, is the DHT22’s practical risk.
  • Soil-moisture quantization: this chapter’s own 12-bit ADC over the 0-100% moisture scale gives \(q = 100/4096 = 0.0244\%\) per count, so \(\sigma_q = 0.0244/\sqrt{12} = 0.00705\%\) – the noise floor built into the reading before any real-world drift, corrosion, or wiring problem adds more.
  • Cross-check against this chapter’s own STUCK threshold: the health monitor flags \(\sigma < 0.01\%\) over 20 readings as STUCK. That threshold sits only \(0.01/0.00705 = 1.42\times\) above the pure quantization floor – confirming why a genuinely live 12-bit sensor practically never reads that quiet, and why an unnaturally low variance is real evidence of a frozen ADC, not a lucky reading.
  • Ideal 12-bit SNR: \(6.02\times12+1.76 = 74.0\) dB – the same figure this codebase derives for any 12-bit ESP32-class ADC, here anchored to the soil-moisture lab’s own bit depth instead of a generic converter.

Averaging (the \(\sigma/\sqrt{N}\) math in the next section) reduces random noise sources such as thermal and flicker noise. It does not rescue a signal sampled too slowly to be represented at all – that failure is fixed only by sampling faster, which is why both limits belong in the same measurement-noise picture.

40.6 Putting Numbers to It

Moving Average Filter Performance

Given noisy sensor data with Gaussian noise \(\sigma = 0.5°C\):

\[\sigma_{filtered} = \frac{\sigma_{raw}}{\sqrt{N}}\]

Window size comparison:

  • \(N=5\): \(\sigma_f = 0.5°C / \sqrt{5} = 0.224°C\) (2.24× improvement)
  • \(N=10\): \(\sigma_f = 0.5°C / \sqrt{10} = 0.158°C\) (3.16× improvement)
  • \(N=20\): \(\sigma_f = 0.5°C / \sqrt{20} = 0.112°C\) (4.47× improvement)

Tradeoff: Larger \(N\) reduces noise but increases lag:

\[\text{Lag} = \frac{N-1}{2} \times \text{sample interval}\]

For \(N=10\) at 1 sample/sec: Lag = 4.5 seconds

Sensor fusion accuracy improvement:

Combining \(M\) independent sensors with equal accuracy \(\sigma\):

\[\sigma_{fused} = \frac{\sigma}{\sqrt{M}}\]

Three DS18B20 sensors (each ±0.5°C):

\[\sigma_{fused} = \frac{0.5°C}{\sqrt{3}} = 0.289°C \text{ (1.73× better)}\]

Hysteresis deadband sizing:

For relay control with ±0.5°C sensor accuracy, minimum safe hysteresis:

\[\Delta T_{hyst} \geq 2 \times \sigma_{sensor} + \text{control margin} = 2 \times 0.5°C + 0.5°C = 1.5°C\]

Example: Turn ON at 28°C, turn OFF at 26.5°C (prevents relay chatter)

Physics PhoebeCheckpoint: Defensive Measurement Math

You now know:

  • A moving average reduces random noise by \(\sqrt{N}\): N=5 gives 2.24x improvement, N=10 gives 3.16x, and N=20 gives 4.47x.
  • Smoothing has a cost: at 1 sample/sec, N=10 adds 4.5 seconds of lag.
  • Three independent DS18B20 sensors reduce a ±0.5°C uncertainty to 0.289°C, and a DHT22 control loop needs at least 1.5°C of hysteresis.

40.6.1 Filter and Fusion Calculator

Use this interactive tool to explore the tradeoffs between moving average window size, sensor fusion count, and hysteresis deadband sizing.

40.7 Hands-On Labs

Lab equipment setup showing development computer connected via USB to ESP32 DevKit, through breadboard with components to multi-sensor array with DHT22, BH1750, BMP280 and DS18B20
Figure 40.2: Laboratory Equipment Setup: Development Computer to Multi-Sensor Array

40.7.1 DHT22 Temperature and Humidity

Objective: Interface DHT22 sensor with ESP32 and implement data filtering.

Materials:

  • ESP32 development board
  • DHT22 sensor
  • 10kOhm pull-up resistor
  • Jumper wires

Circuit:

  • DHT22 VCC to 3.3V
  • DHT22 DATA to GPIO4 (with 10kOhm pull-up to 3.3V)
  • DHT22 GND to GND

What to Observe Before Coding:

The important behavior is not the library call. Watch how the raw temperature and humidity readings jitter, then compare them with the moving average output. Increase the filter window in the simulator below and notice the tradeoff between smoothness and lag.

Optional ESP32 Implementation
#include <DHT.h>

#define DHTPIN 4
#define DHTTYPE DHT22

DHT dht(DHTPIN, DHTTYPE);

// Moving average filter
const int WINDOW_SIZE = 5;
float tempReadings[WINDOW_SIZE];
float humidReadings[WINDOW_SIZE];
int readIndex = 0;
int readCount = 0;  // Track how many readings collected

void setup() {
    Serial.begin(115200);
    dht.begin();

    // Initialize arrays
    for (int i = 0; i < WINDOW_SIZE; i++) {
        tempReadings[i] = 0;
        humidReadings[i] = 0;
    }
}

void loop() {
    float temp = dht.readTemperature();
    float humid = dht.readHumidity();

    if (isnan(temp) || isnan(humid)) {
        Serial.println("Failed to read from DHT sensor!");
        delay(2000);
        return;
    }

    // Store readings
    tempReadings[readIndex] = temp;
    humidReadings[readIndex] = humid;
    readIndex = (readIndex + 1) % WINDOW_SIZE;
    if (readCount < WINDOW_SIZE) readCount++;

    // Calculate moving average (use only filled portion of buffer)
    float tempAvg = 0, humidAvg = 0;
    for (int i = 0; i < readCount; i++) {
        tempAvg += tempReadings[i];
        humidAvg += humidReadings[i];
    }
    tempAvg /= readCount;
    humidAvg /= readCount;

    // Print results
    Serial.print("Raw - Temp: ");
    Serial.print(temp);
    Serial.print("C, Humidity: ");
    Serial.print(humid);
    Serial.println("%");

    Serial.print("Filtered - Temp: ");
    Serial.print(tempAvg);
    Serial.print("C, Humidity: ");
    Serial.print(humidAvg);
    Serial.println("%\n");

    delay(2000);  // DHT22 needs 2 seconds between readings
}

Expected Learning:

  • DHT22 interfacing and timing requirements
  • Implementing moving average filter
  • Handling sensor read errors

The first lab cleaned up one noisy sensor. The next question is what to do when several sensors measure the same quantity and one of them may be wrong.

Try It: Moving Average Filter Simulator

Simulate how a moving average filter smooths noisy sensor data. Adjust the noise level, window size, and base temperature to see the effect in real time.


40.7.2 Lab 2: Multi-Sensor Fusion

Objective: Combine data from multiple temperature sensors for improved accuracy.

Materials:

  • ESP32 development board
  • 3x DS18B20 temperature sensors
  • 4.7kOhm pull-up resistor
  • Jumper wires

Circuit: All sensors on same 1-Wire bus (GPIO5) with 4.7kOhm pull-up

What to Observe Before Coding:

Sensor fusion improves trust by comparing several independent readings. The key questions are: which sensors agree, which sensor looks disconnected, and how much uncertainty remains after combining valid sensors?

Run it: Before wiring three sensors, explore fusion in the calculator below. Choose the Temperature preset to combine several readings and watch the fused estimate and its remaining uncertainty, then switch to Bad sensor to see how one disconnected or drifting sensor is detected and down-weighted. Use it to answer this lab’s observe questions – which sensors agree, which looks disconnected, and how much uncertainty survives fusion – before you write any code.

Optional ESP32 Implementation
#include <OneWire.h>
#include <DallasTemperature.h>

#define ONE_WIRE_BUS 5
#define MAX_SENSORS 8  // Maximum sensors on the bus

OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

void setup() {
    Serial.begin(115200);
    sensors.begin();
    Serial.print("Found ");
    Serial.print(sensors.getDeviceCount());
    Serial.println(" sensor(s)");
}

void loop() {
    sensors.requestTemperatures();

    int numSensors = min((int)sensors.getDeviceCount(), MAX_SENSORS);
    float temps[MAX_SENSORS];
    float weights[MAX_SENSORS];

    Serial.println("Individual Readings:");
    for (int i = 0; i < numSensors; i++) {
        temps[i] = sensors.getTempCByIndex(i);
        weights[i] = 1.0;  // Equal weights

        // Check for disconnected sensor (-127°C)
        if (temps[i] == DEVICE_DISCONNECTED_C) {
            Serial.print("Sensor "); Serial.print(i);
            Serial.println(": DISCONNECTED");
            weights[i] = 0.0;  // Exclude from fusion
            continue;
        }

        Serial.print("Sensor ");
        Serial.print(i);
        Serial.print(": ");
        Serial.print(temps[i]);
        Serial.println("C");
    }

    // Weighted average fusion
    float fusedTemp = 0;
    float totalWeight = 0;
    for (int i = 0; i < numSensors; i++) {
        fusedTemp += temps[i] * weights[i];
        totalWeight += weights[i];
    }
    if (totalWeight == 0) {
        Serial.println("ERROR: No valid sensors!");
        delay(1000);
        return;
    }
    fusedTemp /= totalWeight;

    // Calculate standard deviation (sample std dev with N-1)
    int validSensors = (int)totalWeight;  // Count of sensors with weight > 0
    float variance = 0;
    for (int i = 0; i < numSensors; i++) {
        if (weights[i] > 0) {
            float diff = temps[i] - fusedTemp;
            variance += diff * diff;
        }
    }
    float stdDev = sqrt(variance / max(validSensors - 1, 1));

    Serial.print("\nFused Temperature: ");
    Serial.print(fusedTemp);
    Serial.print("C +/- ");
    Serial.print(stdDev);
    Serial.println("C\n");

    delay(1000);
}

Expected Learning:

  • Multiple sensors on single bus
  • Sensor fusion with weighted average
  • Calculating measurement uncertainty

Physics PhoebeCheckpoint: Fusion and Calibration Evidence

You now know:

  • A 1-Wire bus can carry 3 DS18B20 sensors through one GPIO5 line with a 4.7kOhm pull-up.
  • Fusion starts by excluding disconnected readings such as -127°C, then averaging the valid sensors and reporting the remaining standard deviation.
  • Calibration needs named references: the two-point method uses 0°C and 100°C measurements to compute slope and offset.

40.7.3 Lab 3: Sensor Calibration

Objective: Calibrate temperature sensor using two-point method.

Materials:

  • ESP32 with temperature sensor
  • Ice bath (0°C)
  • Boiling water (100°C)
  • Thermometer (reference)
Two-point calibration procedure showing three steps: ice bath reference at 0 degrees C, boiling water reference at 100 degrees C, and calculation of gain and offset coefficients with safety notes
Figure 40.3: Two-Point Sensor Calibration Procedure: Ice Bath to Boiling Water

Run it: Before you run the two-point procedure, use the calibration decision guide below to confirm two-point is the right method for your deployment. Pick the accuracy stakes (Low risk, Industrial, Regulated, or Safety critical) and the access (Easy access, Remote deployment, or Lab controlled), and read whether one-point, two-point, multi-point, or temperature-compensated calibration is recommended. If the guide points past two-point, note what extra references your procedure would need.

Procedure:

  1. Record ice bath reading: Place sensor in ice bath, record raw value
  2. Record boiling water reading: Place sensor in boiling water, record raw value
  3. Calculate calibration parameters:
Two-Point Calibration Calculation
  • slope = (100 - 0) / (raw_at_100C - raw_at_0C)
  • offset = -slope x raw_at_0C
  • calibrated_temperature = slope x raw_temperature + offset
Optional ESP32 Implementation
#include <OneWire.h>
#include <DallasTemperature.h>

#define ONE_WIRE_BUS 5

OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensor(&oneWire);

// Calibration values (replace with YOUR ice bath and boiling water readings)
const float RAW_AT_0C = 1.2;   // Sensor reading in ice bath
const float RAW_AT_100C = 98.8; // Sensor reading in boiling water

// Calculate calibration parameters
const float SLOPE = 100.0 / (RAW_AT_100C - RAW_AT_0C);
const float OFFSET = -SLOPE * RAW_AT_0C;

void setup() {
    Serial.begin(115200);
    sensor.begin();
    Serial.println("Calibrated Temperature Sensor");
    Serial.print("Slope: "); Serial.println(SLOPE, 4);
    Serial.print("Offset: "); Serial.println(OFFSET, 4);
}

void loop() {
    sensor.requestTemperatures();
    float rawTemp = sensor.getTempCByIndex(0);

    if (rawTemp == DEVICE_DISCONNECTED_C) {
        Serial.println("Sensor disconnected!");
        delay(1000);
        return;
    }

    // Apply calibration
    float calibratedTemp = SLOPE * rawTemp + OFFSET;

    Serial.print("Raw: ");
    Serial.print(rawTemp, 2);
    Serial.print("C | Calibrated: ");
    Serial.print(calibratedTemp, 2);
    Serial.println("C");

    delay(1000);
}

Expected Learning:

  • Two-point calibration method
  • Improving sensor accuracy
  • Linear calibration equations

The calibration lab corrects systematic error. The next lab handles a different problem: readings that should not be trusted at all because the sensor is stuck, disconnected, noisy, or changing faster than the physical system can.

Try It: Two-Point Calibration Calculator

Enter your sensor’s raw readings at two known reference temperatures. The calculator computes the calibration slope and offset, then applies the correction to a test reading.


40.7.4 Lab 4: Sensor Health Monitoring

Objective: Implement sensor health checking and fault detection.

What to Observe Before Coding:

A health check is a decision pipeline. A reading should be rejected if it is physically impossible, if the sensor is stuck, or if the reading jumps faster than the real system can change.

Health-Check Pipeline

Range check -> stuck check -> jump/noise check -> accept or fail over.

Optional ESP32 Implementation
const int HISTORY_SIZE = 10;
float sensorHistory[HISTORY_SIZE];
int historyIndex = 0;
int historyCount = 0;  // How many readings collected so far

bool checkSensorHealth(float reading, float minExpected, float maxExpected) {
    // Check 1: Range check
    if (reading < minExpected || reading > maxExpected) {
        Serial.println("ERROR: Reading out of range!");
        return false;
    }

    // Store reading
    sensorHistory[historyIndex] = reading;
    historyIndex = (historyIndex + 1) % HISTORY_SIZE;
    if (historyCount < HISTORY_SIZE) historyCount++;

    // Need enough history for statistical checks
    if (historyCount < 3) {
        Serial.println("Sensor OK (collecting history...)");
        return true;
    }

    // Check 2: Stuck sensor (all collected values identical)
    bool allSame = true;
    for (int i = 1; i < historyCount; i++) {
        if (abs(sensorHistory[i] - sensorHistory[0]) > 0.01) {
            allSame = false;
            break;
        }
    }
    if (allSame) {
        Serial.println("ERROR: Sensor stuck (constant value)!");
        return false;
    }

    // Check 3: Excessive noise (large jump between consecutive readings)
    float maxChange = 5.0;  // Maximum expected change between readings
    for (int i = 1; i < historyCount; i++) {
        float change = abs(sensorHistory[i] - sensorHistory[i - 1]);

        if (change > maxChange) {
            Serial.println("WARNING: Excessive noise detected!");
            return false;
        }
    }

    Serial.println("Sensor HEALTHY");
    return true;
}

void setup() {
    Serial.begin(115200);
    // Initialize history
    for (int i = 0; i < HISTORY_SIZE; i++) {
        sensorHistory[i] = 0;
    }
}

void loop() {
    float temp = readTemperature();

    if (checkSensorHealth(temp, 0.0, 50.0)) {
        // Use the reading
        processData(temp);
    } else {
        // Handle sensor failure
        useBackupSensor();
    }

    delay(1000);
}

Expected Learning:

  • Sensor fault detection methods
  • Range checking and validation
  • Detecting stuck or noisy sensors

40.7.5 Temperature Relay Hysteresis

Objective: Implement hysteresis-based relay control to prevent rapid switching.

Relays are electrically operated switches that allow low-power microcontrollers to control high-power AC/DC devices safely. This lab demonstrates how to combine sensor reading with relay control using hysteresis – a critical pattern for home automation and industrial control.

As calculated in the “Putting Numbers to It” section above, a DHT22 with ±0.5°C accuracy needs at least 1.5°C of hysteresis to avoid relay chatter. This example uses a 2°C deadband (ON at 28°C, OFF at 26°C), providing comfortable margin above the 1.5°C minimum.

Application: Temperature-Controlled Fan

Before reading code, use the hysteresis simulator below. Try a zero or tiny deadband and watch the relay chatter. Then increase the deadband until switching becomes stable.

Optional ESP32 Implementation
#include <DHT.h>

#define DHT_PIN 4
#define RELAY_PIN 13

#define TEMP_THRESHOLD_ON 28.0   // Turn fan ON above 28C
#define TEMP_THRESHOLD_OFF 26.0  // Turn fan OFF below 26C (hysteresis)

DHT dht(DHT_PIN, DHT22);
bool fanState = false;

void setup() {
  Serial.begin(115200);

  pinMode(RELAY_PIN, OUTPUT);
  digitalWrite(RELAY_PIN, LOW);  // Fan initially OFF

  dht.begin();

  Serial.println("Temperature-Controlled Fan System");
}

void loop() {
  float temperature = dht.readTemperature();
  float humidity = dht.readHumidity();

  if (isnan(temperature) || isnan(humidity)) {
    Serial.println("Failed to read from DHT sensor!");
    delay(2000);
    return;
  }

  // Control logic with hysteresis to prevent rapid switching
  if (!fanState && temperature > TEMP_THRESHOLD_ON) {
    // Turn fan ON
    fanState = true;
    digitalWrite(RELAY_PIN, HIGH);
    Serial.println("Temperature HIGH - Fan turned ON");
  }
  else if (fanState && temperature < TEMP_THRESHOLD_OFF) {
    // Turn fan OFF
    fanState = false;
    digitalWrite(RELAY_PIN, LOW);
    Serial.println("Temperature OK - Fan turned OFF");
  }

  // Display status
  Serial.print("Temp: ");
  Serial.print(temperature, 1);
  Serial.print("C | Humidity: ");
  Serial.print(humidity, 1);
  Serial.print("% | Fan: ");
  Serial.println(fanState ? "ON" : "OFF");

  delay(2000);
}
Learning Points: Hysteresis Control

Why Hysteresis Matters:

Without hysteresis, a fan relay can chatter around the threshold: 27.9 C off, 28.1 C on, 27.9 C off again. That rapid switching is noisy, confusing, and can damage the relay.

With hysteresis, the fan may turn on at 28 C but must cool down to 26 C before turning off. The control action becomes stable instead of nervous.

Real-World Applications:

  • HVAC control: Heating/cooling systems with thermostats
  • Irrigation systems: Pump control based on soil moisture
  • Industrial automation: Motor control, conveyor systems

Expected Learning:

  • Relay control with GPIO output
  • Hysteresis to prevent relay chatter
  • Combining sensor input with actuator output

Physics PhoebeCheckpoint: Stable Control from Noisy Data

You now know:

  • Without hysteresis, a relay can chatter around 28°C; with hysteresis, it turns on at 28°C and waits until 26°C before turning off.
  • The 2°C deadband in this lab is intentionally above the 1.5°C minimum calculated from the DHT22 accuracy.
  • A sensor pipeline is not complete until validation, filtering, calibration, health checks, and actuator rules agree on what to do with a reading.
Try It: Hysteresis Control Simulator

Watch how hysteresis prevents rapid relay switching. The simulator generates a temperature signal that oscillates around your setpoint, and shows when the relay turns ON/OFF with and without hysteresis.

40.8 Knowledge Check


## Missing Sensor Health Monitoring {.depth-l2}

::: {.callout-warning}
**The Mistake**: A smart agriculture system deploys 50 soil moisture sensors across a farm. Several sensors gradually fail over months due to corrosion from fertilizers, but the system continues reporting the last valid reading or garbage data. Farmers unknowingly over-water or under-water crops, reducing yields by 15-20% before the problem is discovered during routine maintenance.

**Why It Happens**: Developers focus on the "happy path" — reading sensors when everything works perfectly — but ignore failure modes. Real deployments face harsh environments, loose connections, power fluctuations, and sensor degradation. Without health monitoring, you can't distinguish between "sensor reports low moisture" (needs water) and "sensor is dead/stuck" (needs replacement).

**Real-World Failure Modes**:

::: {.slbp-mobile-cards .slbp-failure-table}

| Failure Type | Symptom | Detection Method |
|:-------------|:--------|:----------------|
| **Disconnected sensor** | Reading stuck at 0 or max value | Range check + variance check |
| **Corroded contacts** | Erratic readings, large fluctuations | Standard deviation > threshold |
| **Short circuit** | Always returns same value | Zero variance over time |
| **Intermittent connection** | Occasional "impossible" readings | Rate-of-change check |
| **ADC saturation** | Always at 0 or 4095 (12-bit ADC) | Exact boundary value check |
| **Power brownout** | Multiple sensors fail simultaneously | Cross-sensor correlation |

:::

**The Fix: Implement Multi-Layer Sensor Health Monitoring**

**Layer 1: Immediate Range Validation**
```cpp
bool isReadingValid(float moisture, float tempC) {
    // Moisture: 0-100% valid range
    if (moisture < 0 || moisture > 100) {
        logError("Moisture out of range: " + String(moisture));
        return false;
    }

    // Temperature: -40 to +85C for outdoor sensors
    if (tempC < -40 || tempC > 85) {
        logError("Temperature out of range: " + String(tempC));
        return false;
    }

    // Cross-check: unlikely combinations (adjust for your environment)
    if (tempC < -10 && moisture > 80) {
        // Very high soil moisture unlikely in deeply frozen ground
        logError("Unlikely temp-moisture combo");
        return false;
    }

    return true;
}

Layer 2: Statistical Variance Checking

class SensorHealthMonitor {
private:
    static const int HISTORY_SIZE = 20;
    float readings[HISTORY_SIZE];
    int index = 0;
    int validCount = 0;
    unsigned long lastUpdateTime = 0;

public:
    enum HealthStatus { HEALTHY, STUCK, ERRATIC, NO_DATA };

    HealthStatus checkHealth(float newReading) {
        unsigned long now = millis();

        // Check for stale data (no update in 60 seconds)
        if (now - lastUpdateTime > 60000 && validCount > 0) {
            return NO_DATA;
        }

        readings[index] = newReading;
        index = (index + 1) % HISTORY_SIZE;
        if (validCount < HISTORY_SIZE) validCount++;
        lastUpdateTime = now;

        if (validCount < 5) {
            return HEALTHY; // Not enough data yet
        }

        // Calculate mean and standard deviation
        float sum = 0, sumSq = 0;
        for (int i = 0; i < validCount; i++) {
            sum += readings[i];
            sumSq += readings[i] * readings[i];
        }
        float mean = sum / validCount;
        float variance = (sumSq / validCount) - (mean * mean);
        float stdDev = sqrt(variance);

        // Check 1: Stuck sensor (zero variance)
        if (stdDev < 0.01) {
            return STUCK;
        }

        // Check 2: Erratic sensor (excessive variance)
        // For soil moisture, expect stdDev < 5% under normal conditions
        if (stdDev > 10.0) {
            return ERRATIC;
        }

        return HEALTHY;
    }
};

Layer 3: Rate-of-Change Validation

bool checkRateOfChange(float newValue, float previousValue, float maxChangePerSecond, unsigned long deltaTimeMs) {
    float deltaValue = abs(newValue - previousValue);
    float deltaTimeSeconds = deltaTimeMs / 1000.0;
    float rate = deltaValue / deltaTimeSeconds;

    if (rate > maxChangePerSecond) {
        Serial.print("Excessive rate of change: ");
        Serial.print(rate);
        Serial.print(" per second (max: ");
        Serial.print(maxChangePerSecond);
        Serial.println(")");
        return false;
    }

    return true;
}

// Example: soil moisture cannot change by more than 5% per minute
// (even heavy rain takes time to soak in)
if (!checkRateOfChange(moisture, lastMoisture, 5.0/60.0, deltaTime)) {
    // Sensor reading is suspect, use previous value
    moisture = lastMoisture;
}

Layer 4: Cross-Sensor Correlation

// If you have multiple sensors in similar conditions
bool checkCrossSensorConsistency(float sensors[], int count) {
    // Calculate mean and standard deviation
    float sum = 0;
    for (int i = 0; i < count; i++) sum += sensors[i];
    float mean = sum / count;

    float sumSq = 0;
    for (int i = 0; i < count; i++) {
        float diff = sensors[i] - mean;
        sumSq += diff * diff;
    }
    float stdDev = sqrt(sumSq / max(count - 1, 1));

    // Use absolute threshold (works for any value range, including near-zero)
    float threshold = max(stdDev * 2.5, 3.0);  // At least 3 units deviation

    // Check if any sensor is an outlier
    for (int i = 0; i < count; i++) {
        float deviation = abs(sensors[i] - mean);
        if (deviation > threshold) {
            Serial.print("Sensor ");
            Serial.print(i);
            Serial.println(" may be faulty (outlier)");
            return false;
        }
    }

    return true;
}

Layer 5: Self-Diagnostics and Recovery

void handleSensorFailure(int sensorID, HealthStatus status) {
    // Log failure with timestamp
    logToSD("Sensor " + String(sensorID) + " health: " + String(status));

    // Send alert to cloud/user
    sendMQTTAlert("sensor/" + String(sensorID) + "/health", status);

    // Attempt recovery strategies
    switch(status) {
        case STUCK:
            // Try power cycling the sensor
            powerCycleSensor(sensorID);
            delay(5000);
            // Retry reading
            break;

        case ERRATIC:
            // Increase filtering window
            useMedianFilterForSensor(sensorID, true);
            break;

        case NO_DATA:
            // Check I2C/SPI bus health
            scanI2CBus();
            // Try re-initializing sensor
            initializeSensor(sensorID);
            break;
    }

    // If recovery fails, use fallback data source
    if (!sensorIsHealthy(sensorID)) {
        useBackupSensor(sensorID);
        // Or use interpolated value from neighboring sensors
        // Or use last known good value with warning flag
    }
}

Complete Implementation Example:

void loop() {
    float moisture = readSoilMoisture();
    float temp = readTemperature();

    // Layer 1: Range validation
    if (!isReadingValid(moisture, temp)) {
        handleSensorFailure(MOISTURE_SENSOR_ID, ERRATIC);
        return;
    }

    // Layer 2: Statistical health check
    HealthStatus health = moistureMonitor.checkHealth(moisture);
    if (health != HEALTHY) {
        handleSensorFailure(MOISTURE_SENSOR_ID, health);
        return;
    }

    // Layer 3: Rate-of-change check
    if (!checkRateOfChange(moisture, lastMoisture, 5.0/60.0, deltaTime)) {
        handleSensorFailure(MOISTURE_SENSOR_ID, ERRATIC);
        moisture = lastMoisture; // Use previous valid value
    }

    // Layer 4: Cross-sensor correlation (if available)
    float nearbyMoisture[] = {moisture, readNearbySensor1(), readNearbySensor2()};
    if (!checkCrossSensorConsistency(nearbyMoisture, 3)) {
        logWarning("Cross-sensor inconsistency detected");
    }

    // If all checks pass, use the data
    updateIrrigationControl(moisture, temp);

    lastMoisture = moisture;
    lastTemp = temp;
    lastReadingTime = millis();
}

Key Metrics to Monitor:

Metric Threshold Action
Variance σ < 0.01 for 20 readings STUCK - power cycle
Variance σ > 10.0 ERRATIC - increase filtering
Update rate No data for 60 seconds NO_DATA - reinitialize
Range Outside physical limits INVALID - discard reading
Rate of change >5%/min for soil moisture SUSPECT - use previous value
Cross-sensor deviation >2.5 sigma or >3 units from peer mean OUTLIER - flag for maintenance

Cost-Benefit Analysis:

  • Implementation time: +4 hours of development
  • Code overhead: +2KB flash, +200 bytes RAM
  • Benefit: Prevents 15-20% crop yield loss ($5,000+ value per season)
  • Benefit: Reduces false alarms by 90% (saves farmer time)
  • Benefit: Early detection of failing sensors (replace proactively, not reactively)

Real Deployment Results: After adding health monitoring to a 50-sensor farm system:

  • Detected 3 sensors with corroded contacts within first week
  • Identified 1 sensor with intermittent connection (loose wire)
  • Prevented one over-watering incident that would have cost $1,200 in water + crop damage
  • Reduced maintenance visits from monthly to quarterly

Key Lesson: Sensor health monitoring is not optional for production IoT systems. The cost of adding these checks (4 hours of development) is trivial compared to the cost of acting on bad data (thousands in damage, lost crops, false alarms). Always implement at minimum: range validation, stuck sensor detection, and rate-of-change checks. :::

Physics PhoebeCheckpoint: Sensor Health Decisions

You now know:

  • Range checks reject impossible values before they pollute buffers, filters, or control decisions.
  • Variance checks catch stuck sensors: sigma below 0.01 for 20 readings is a STUCK condition in the farm example.
  • Production health monitoring combines stale-data limits (60 seconds), rate-of-change checks (>5%/min for soil moisture), and cross-sensor outlier checks (>2.5 sigma or >3 units).

40.9 Label the Diagram

40.10 Code Challenge

40.11 Noise and Averaging Limits

The lab above practices validation, filtering, fusion, calibration, health monitoring, and hysteresis. The companion page explains the measurement-noise boundary behind those practices: thermal noise, flicker noise, quantization, SNR, averaging, drift, and the point where calibration evidence matters more than more samples.

Next Measurement Practice

Continue with Sensor Noise and Averaging Limits to decide when averaging improves a reading and when the right fix is placement, bandwidth, grounding, reference checks, or calibration.

40.12 Summary

This chapter covered sensor implementation best practices and hands-on labs:

  • Validation and filtering ensure data quality through range checks, moving averages, and spike removal
  • Multi-stage pipelines process raw sensor data through validation, filtering, fusion, and storage
  • DHT22 lab demonstrated proper interfacing with timing requirements and moving average filtering
  • Sensor fusion lab showed how multiple sensors improve accuracy through weighted averaging
  • Calibration lab taught two-point calibration to correct offset and gain errors
  • Health monitoring lab detects stuck sensors, excessive noise, and out-of-range readings
  • Relay lab with hysteresis prevents rapid switching in threshold-based control systems
Key Takeaway

Production-quality sensor implementations require multiple defensive layers: validate every reading (check for NaN and out-of-range values), filter noise with moving averages, fuse multiple sensors to improve accuracy and reliability, and use hysteresis in control logic to prevent rapid relay switching. A sensor that appears to work on your bench may fail in the field without these safeguards.

For Kids: Meet the Sensor Squad!

Sammy the Sensor was excited to measure the temperature of a garden, but sometimes his readings were a bit jumpy – one second he said 25 degrees, the next he said 30, then back to 24!

“That is just noise,” explained Max the Microcontroller. “Let me help you smooth things out.” Max took Sammy’s last five readings and averaged them together. “See? Now we get a nice steady 25 degrees!”

But Bella the Battery was worried. “What if Sammy gets confused and says it is 500 degrees? That is impossible!” So Max added a rule: if any reading is too crazy (like below -40 or above 80), just throw it away and try again.

Then Lila the LED had a great idea: “What if we use THREE temperature friends instead of just one? If two of them agree and the third one is way off, we know which one is having a bad day!” That is called sensor fusion, and it makes the whole Sensor Squad much more reliable!

40.13 What’s Next?

Continue to the comprehensive calibration lab with Wokwi simulation for hands-on practice with professional calibration techniques, or explore related sensor topics below.

Chapter Description
Sensor Noise and Averaging Limits SNR, thermal noise, flicker noise, ADC quantization, averaging limits, drift, and calibration boundaries
Calibration Lab (Wokwi) Hands-on calibration with interactive ESP32 simulation
Temperature Sensor Labs Temperature sensing fundamentals and DHT/DS18B20 interfacing
Motion & Environmental Sensors IMU, barometric, and environmental sensor labs
Light & Proximity Sensors Light intensity and distance measurement techniques
Sensor Specifications Core sensor specifications: accuracy, precision, and linearity
Sensor Signal Conditioning Amplification, filtering, and ADC conversion fundamentals