Lesson 4.3 — Control Actuators Based on Sensor Inputs to Achieve Desired System Behaviour
The full power of IoT is realised when sensor data drives real-world actions. This lesson builds complete closed-loop systems — systems where the output is automatically determined by the sensor input according to defined logic.
Open Loop vs Closed Loop Control
| Property |
Open Loop (Timer-Based) |
Closed Loop (Feedback-Based) |
| Definition |
Output action is predetermined, not affected by sensor feedback |
Sensor feedback continuously adjusts the output to reach a desired state |
| Example |
Timer-controlled irrigation: water for 10 minutes every day |
Soil moisture sensor irrigation: water until soil reaches 70% moisture |
| Stability |
Drifts over time if conditions change |
Self-correcting, adapts to changes |
| Complexity |
Simple to implement |
Requires sensor + threshold + actuator + feedback loop |
| IoT example |
Time-based street light schedule |
LDR-based street light that responds to actual darkness |
KEY
All control systems in this lesson are closed-loop: the sensor measures a condition, the program compares it to a setpoint, and the actuator corrects the condition. This is the foundational model for smart systems everywhere — from thermostats to self-driving cars.
Project 1: Smart Temperature-Controlled Fan
| Component |
Connection |
Role |
| DHT22 DATA |
Arduino pin 4 |
Temperature and humidity source |
| L298N ENA |
Arduino pin 9 (PWM) |
Fan speed via PWM (0–255) |
| L298N IN1 |
Arduino pin 7 |
Motor direction |
| L298N IN2 |
Arduino pin 8 |
Motor direction (LOW for forward) |
| Status LED |
Arduino pin 13 |
HIGH when fan running |
| L298N motor power |
12 V external supply |
Motor supply (separate from Arduino 5 V) |
C/C++ — Smart Temperature-Controlled Fan System
#include <DHT.h>
#define DHT_PIN 4
#define ENA_PIN 9 // PWM speed control
#define IN1_PIN 7
#define IN2_PIN 8
#define LED_PIN 13
DHT dht(DHT_PIN, DHT22);
const float FAN_ON_TEMP = 28.0; // °C: turn fan ON above this
const float FAN_OFF_TEMP = 25.0; // °C: turn fan OFF below this (hysteresis)
const int FAN_SPEED = 200; // PWM: 0–255
bool fanRunning = false;
unsigned long lastRead = 0;
const long READ_INTERVAL = 3000;
void setFan(bool on, int speed = FAN_SPEED) {
if (on) {
digitalWrite(IN1_PIN, HIGH); digitalWrite(IN2_PIN, LOW);
analogWrite(ENA_PIN, speed); digitalWrite(LED_PIN, HIGH);
} else {
analogWrite(ENA_PIN, 0); digitalWrite(LED_PIN, LOW);
}
fanRunning = on;
}
void setup() {
Serial.begin(9600); dht.begin();
pinMode(IN1_PIN, OUTPUT); pinMode(IN2_PIN, OUTPUT); pinMode(LED_PIN, OUTPUT);
setFan(false);
Serial.println("Fan controller ready.");
}
void loop() {
if (millis() - lastRead >= READ_INTERVAL) {
lastRead = millis();
float temp = dht.readTemperature();
float hum = dht.readHumidity();
if (isnan(temp)) { Serial.println("Sensor error — maintaining last fan state"); return; }
Serial.print("T:"); Serial.print(temp,1); Serial.print("C H:"); Serial.print(hum,1);
Serial.print("% Fan:"); Serial.println(fanRunning ? "ON" : "OFF");
// Hysteresis control
if (!fanRunning && temp >= FAN_ON_TEMP) { setFan(true); Serial.println(">>> Fan ON"); }
else if (fanRunning && temp <= FAN_OFF_TEMP) { setFan(false); Serial.println(">>> Fan OFF"); }
}
}
Project 2: Automatic Night Light (LDR + PWM LED)
An LDR measures ambient light. The LED brightness is continuously and inversely proportional to the light level — the darker the environment, the brighter the LED. This is a proportional (P) controller, the simplest form of closed-loop feedback control.
C/C++ — Automatic Night Light with Smooth Dimming
#define LDR_PIN A0
#define LED_PIN 9 // PWM pin
int ledBrightness = 0;
int ldrSmooth = 0;
unsigned long lastUpdate = 0;
void setup() { Serial.begin(9600); pinMode(LED_PIN, OUTPUT); }
void loop() {
if (millis() - lastUpdate >= 100) {
lastUpdate = millis();
int rawLDR = analogRead(LDR_PIN);
ldrSmooth = (ldrSmooth * 7 + rawLDR) / 8; // Exponential moving average
// high light (bright ~800) → dim LED | low light (dark ~100) → bright LED
int targetBrightness = constrain(map(ldrSmooth, 100, 800, 255, 0), 0, 255);
// Gradual transition (avoid sudden jumps)
if (ledBrightness < targetBrightness) ledBrightness++;
else if (ledBrightness > targetBrightness) ledBrightness--;
analogWrite(LED_PIN, ledBrightness);
Serial.print("LDR:"); Serial.print(ldrSmooth);
Serial.print(" LED:"); Serial.println(ledBrightness);
}
}
TRY
Open the Serial Plotter (Tools → Serial Plotter) while this code is running and cover/uncover the LDR with your hand. You will see the LDR and LED lines move inversely in real time — closed-loop feedback visualised.
Project 3: Smart Security System — State Machine with Multi-Actuator Response
| State |
Behaviour |
| IDLE |
No motion, no close object. All outputs off. Check PIR every loop, HC-SR04 every 200 ms. |
| ALERT |
PIR triggered. LED blinks slowly (500 ms), buzzer beeps once, relay stays off. If HC-SR04 detects object within 50 cm: upgrade to ALARM. |
| ALARM |
Close intrusion confirmed. LED fast-blink (100 ms), continuous buzzer tone, relay ON (simulates siren/camera). Return to IDLE only after 10 s with no further motion. |
C/C++ — Security System State Machine
#define PIR_PIN 5
#define TRIG_PIN 9
#define ECHO_PIN 10
#define LED_PIN 6
#define BUZZER_PIN 8
#define RELAY_PIN 7
enum State { IDLE, ALERT, ALARM };
State currentState = IDLE;
unsigned long stateEnteredAt = 0;
unsigned long lastBlinkTime = 0;
bool ledState = false;
long measureDistance() {
digitalWrite(TRIG_PIN, LOW); delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH); delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
long d = pulseIn(ECHO_PIN, HIGH, 20000);
return d == 0 ? 999 : d * 0.0343 / 2;
}
void setState(State s) {
currentState = s; stateEnteredAt = millis();
noTone(BUZZER_PIN); digitalWrite(RELAY_PIN, LOW); digitalWrite(LED_PIN, LOW);
Serial.print("State → ");
Serial.println(s == IDLE ? "IDLE" : s == ALERT ? "ALERT" : "ALARM");
}
void setup() {
Serial.begin(9600);
pinMode(PIR_PIN, INPUT);
pinMode(TRIG_PIN, OUTPUT); pinMode(ECHO_PIN, INPUT);
pinMode(LED_PIN, OUTPUT); pinMode(BUZZER_PIN, OUTPUT); pinMode(RELAY_PIN, OUTPUT);
setState(IDLE);
}
void loop() {
unsigned long now = millis();
bool motion = digitalRead(PIR_PIN) == HIGH;
long distCm = measureDistance();
switch (currentState) {
case IDLE:
if (motion) setState(ALERT);
break;
case ALERT:
// Slow LED blink
if (now - lastBlinkTime >= 500) {
lastBlinkTime = now; ledState = !ledState; digitalWrite(LED_PIN, ledState);
}
if (distCm < 50) setState(ALARM);
if (!motion && (now - stateEnteredAt > 10000)) setState(IDLE);
break;
case ALARM:
// Fast LED blink + continuous alarm
if (now - lastBlinkTime >= 100) {
lastBlinkTime = now; ledState = !ledState; digitalWrite(LED_PIN, ledState);
}
tone(BUZZER_PIN, 2000);
digitalWrite(RELAY_PIN, HIGH);
if (!motion && (now - stateEnteredAt > 10000)) setState(IDLE);
break;
}
}
KEY
A state machine is a fundamental design pattern for IoT control systems. Instead of a tangle of nested if/else statements, the system exists in one well-defined state at a time, and transitions between states based on sensor events. This makes the system easier to understand, test, and extend.
Actuator Safety Guidelines
| Safety rule |
Explanation |
| Never exceed GPIO current limits |
Arduino/ESP32 GPIO pins source/sink max 40 mA each, 200 mA total. Always use driver transistors or relay modules for loads above 20 mA. |
| Flyback diodes on inductive loads |
Motors, relays, and solenoids produce back-EMF spikes when switched off. Always place a flyback diode (1N4007) across the coil: cathode toward +V, anode toward GND. |
| Separate power supplies for motors |
DC motors draw large transient currents at startup (3–5× running current). Use a separate supply — powering a motor from the MCU supply causes voltage sags that reset the MCU. |
| Galvanic isolation for mains |
Relay modules provide galvanic isolation between the MCU (5 V) and mains (230 V). Optocoupler-based relay modules are safer — they optically isolate the control and power sides. |
| Heat dissipation |
High-power actuators and driver ICs generate heat. L298N running a motor may need a heatsink on the IC package. |
| Test without loads first |
Verify your control logic works (LED as placeholder) before connecting real loads like pumps or motors. This prevents accidents when code has bugs. |
Check Your Understanding — Lesson 4.3
?
1. In the smart fan controller, why is hysteresis used (28°C to turn on, 25°C to turn off) rather than a single threshold (e.g. 27°C for both)? What practical problem does this solve?
?
2. Draw the state diagram for the security system (IDLE, ALERT, ALARM states). Label each state with its active outputs and each arrow with its transition condition.
?
3. A student connects a 12 V solenoid valve directly to an ESP32 GPIO pin set HIGH. List THREE problems this will cause and explain the correct circuit topology to safely control the solenoid from the ESP32.