Community project

Humidity-Temperature Motor Control

ESP32
Photo of Humidity-Temperature Motor Control
Generated with AI

miji ibrahim

Last updated October 6, 2026

This project builds a smart motor controller that responds to environmental conditions by reading temperature and humidity from a DHT11 sensor and light levels from a digital light sensor. The system displays real-time readings on a 16x2 LCD screen and automatically triggers a 12V brushed DC motor based on the sensor data, making it ideal for applications like automated window blinds, ventilation fans, or other climate-responsive mechanisms.

The guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions for integrating the BTS7960 high-current motor driver with the ESP32 microcontroller. Included is the full Arduino firmware that handles sensor sampling, LCD display updates, and motor control logic with timing management for smooth operation.

Wiring diagram

Wiring diagram for Humidity-Temperature Motor Control

Gather all the parts

QtyComponent
1

DHT11

Digital temperature and humidity sensor (lower accuracy than DHT22)

1

12 V brushed DC motor

A two-wire 12 V brushed DC motor that provides the requested movement.

1

12V Barrel-Jack Adapter

12 V / 2 A wall adapter with a 5.5 mm / 2.1 mm barrel jack. Used to power motor drivers, LED strips, or boards that need a higher rail.

1

24v Buck Converter

LM2596-based adjustable step-down buck converter module. Commonly used to regulate a higher battery rail, such as a 2S 18650 pack, down to 5V for Arduino logic. It is a regulator, not a charger or battery protection board.

1

BSS138 4-channel bidirectional logic level shifter

A small board that safely translates the LCD's 5 V I²C wires to the ESP32's 3.3 V signal level.

1

3-pin digital light sensor module

A small light-sensing module that gives a simple on/off signal when light crosses its adjustable threshold.

1

LCD 16x2 I2C

16x2 character LCD display with I2C backpack

1

IBT-2 BTS7960 43A High-Current H-Bridge Motor Driver

IBT-2 high-current H-bridge motor driver module built around BTS7960 half-bridge devices. Common modules are sold for 6V-27V motor supplies and 5V logic, with 43A as a module headline rating; practical continuous current depends on board cooling and wiring. Direction and speed are controlled with RPWM/LPWM PWM inputs plus R_EN/L_EN enable lines. Logic-side VCC is separate from the high-power motor supply, and grounds must be common.

Assemble it in 7 steps

1. Turn all power off

Unplug the ESP32 USB cable and switch off or unplug the 12 V adapter before moving any wires. This prevents the motor from starting unexpectedly while you rewire it.

  • Do not work on the motor wires with the 12 V adapter connected — the motor can start suddenly and a loose wire can short the supply.

2. Keep the sensor wires connected

Connect DHT11 VCC to ESP32 3V3 (power), DHT11 GND to ESP32 GND (ground), and DHT11 DATA to GPIO4 (temperature and humidity signal). Connect the light sensor VCC to ESP32 3V3 (power), GND to ESP32 GND (ground), and DO to GPIO23 (light signal).

  • Power the light sensor from 3.3 V so its signal is safe for the ESP32.

3. Keep the LCD level shifter connected

Connect LCD VCC to the regulated 5 V buck output (power) and LCD GND to shared GND (ground). On the LCD level shifter, connect LV to ESP32 3V3 (power), HV to regulated 5 V (power), and GND to shared GND (ground). Connect LV1 to GPIO21 (LCD data), HV1 to LCD SDA (data), LV2 to GPIO22 (LCD clock), and HV2 to LCD SCL (clock).

  • Do not connect a 5 V LCD SDA or SCL wire directly to GPIO21 or GPIO22 — 5 V can damage the ESP32.

4. Remove the old motor driver and shifter

Remove the HC-160A motor driver and the motor-control BSS138 level-shifter from the motor circuit. The BTS7960 replaces both of them, so do not reuse the old PA, A, B, or BSS138 motor-control wires.

  • Keep the separate LCD level shifter installed; only the motor-control shifter is removed.

5. Connect the BTS7960 control side

Connect BTS7960 VCC to regulated 5 V (logic power), BTS7960 GND to ESP32 GND (shared ground), RPWM to GPIO27 (UP command), and LPWM to GPIO14 (DOWN command). Connect both R_EN and L_EN to ESP32 3V3 (enable signals).

  • Do not connect 5 V or 12 V to GPIO27 or GPIO14 — those ESP32 pins accept only 3.3 V signals.

6. Connect the motor and 12 V supply

Connect the 12 V adapter +12V lead to BTS7960 B+ (motor power) and the adapter GND lead to BTS7960 B− (motor ground). Connect the motor’s two leads to BTS7960 M+ and M− (motor outputs). Connect the same adapter +12V to buck VIN+ (power) and adapter GND to buck VIN− (ground).

  • Never connect the 12 V adapter to ESP32 VIN, 3V3, or any GPIO pin — 12 V will damage the board.

7. Set the 5 V output and power up

Use a meter to set the buck converter output to exactly 5.0 V before connecting it. Connect buck VOUT+ to ESP32 VIN/5V (power) and buck VOUT− to ESP32 GND (ground). Then connect USB to the ESP32 and turn on the 12 V adapter.

  • A buck output above 5.0 V can damage the ESP32 and the BTS7960 logic side.

Review all connections

1. Connections between "dht11_1" and "ESP32"

Functiondht11_1ESP32
powerVCC3V3
groundGNDGND
dataDATAGPIO 4

2. Connections between "light_sensor_1" and "ESP32"

Functionlight_sensor_1ESP32
powerVCC3V3
groundGNDGND
digitalDOGPIO 23

3. Connections between "lcd_16x2_1" and "ESP32"

Functionlcd_16x2_1ESP32
powerVCCVIN
groundGNDGND

4. Connections between "level_shifter_lcd_1" and "ESP32"

Functionlevel_shifter_lcd_1ESP32
powerLV3V3
powerHVVIN
groundGNDGND
i2cLV1GPIO 21
i2cHV1 → LCD 16x2 I2C SDAEXT
i2cLV2GPIO 22
i2cHV2 → LCD 16x2 I2C SCLEXT

5. Connections between "supply_12v_1" and "ESP32"

Functionsupply_12v_1ESP32
power+12V → 24v Buck Converter VIN+EXT
groundGND → 24v Buck Converter VIN-EXT

6. Connections between "buck_5v_1" and "ESP32"

Functionbuck_5v_1ESP32
powerVOUT+VIN
groundVOUT-GND

7. Connections between "bts7960_1" and "ESP32"

Functionbts7960_1ESP32
powerVCCVIN
groundGNDGND
pwmRPWMGPIO 27
pwmLPWMGPIO 14
powerR_EN3V3
powerL_EN3V3
powerB+ → 12V Barrel-Jack Adapter +12VEXT
groundB- → 12V Barrel-Jack Adapter GNDEXT
dataM+ → 12 V brushed DC motor M+EXT
dataM- → 12 V brushed DC motor M-EXT

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <DHT.h>
#include <LiquidCrystal_I2C.h>

constexpr uint8_t DHT_PIN = 4;
constexpr uint8_t LIGHT_SENSOR_PIN = 23;
// BTS7960: RPWM drives UP and LPWM drives DOWN. The opposite PWM pin is held LOW.
constexpr uint8_t MOTOR_RPWM_PIN = 27;
constexpr uint8_t MOTOR_LPWM_PIN = 14;
constexpr uint8_t I2C_SDA_PIN = 21;
constexpr uint8_t I2C_SCL_PIN = 22;
constexpr uint8_t DHT_TYPE = DHT11;
constexpr uint8_t LCD_ADDRESS = 0x27;
constexpr unsigned long SAMPLE_INTERVAL_MS = 2000;
constexpr unsigned long MOTOR_MOVE_TIME_MS = 1000;
constexpr uint8_t MOTOR_PWM_50_PERCENT = 128;
constexpr unsigned long STARTUP_ANIMATION_INTERVAL_MS = 300;
constexpr unsigned long STARTUP_DURATION_MS = 3000;
constexpr unsigned long MOTOR_MESSAGE_INTERVAL_MS = 160;
// Keep the movement notice visible for three seconds, even though the motor runs for one second.
constexpr unsigned long MOTOR_MESSAGE_DURATION_MS = 3000;
constexpr unsigned long IDLE_TITLE_INTERVAL_MS = 220;
constexpr unsigned long IDLE_TITLE_PERIOD_MS = 10000;
constexpr unsigned long IDLE_TITLE_DURATION_MS = 3000;
constexpr char STARTUP_TITLE[] = "Smart Power Window Controller";

DHT dht(DHT_PIN, DHT_TYPE);
LiquidCrystal_I2C lcd(LCD_ADDRESS, 16, 2);
unsigned long lastSampleMs = 0;
unsigned long motorStartMs = 0;
unsigned long lastStartupAnimationMs = 0;
unsigned long startupStartMs = 0;
bool motorMoving = false;
bool startupActive = true;
uint8_t startupTitleOffset = 0;
bool startupFlashOn = false;
unsigned long lastMotorMessageMs = 0;
unsigned long motorMessageStartMs = 0;
bool motorMessageActive = false;
uint8_t motorMessageOffset = 0;
unsigned long lastMotorOutputChangeMs = 0;
unsigned long idleTitleStartMs = 0;
unsigned long lastIdleTitleAnimationMs = 0;
bool idleTitleActive = false;
bool idleTitleFlashOn = false;
uint8_t idleTitleOffset = 0;
bool haveValidReading = false;

enum class MotorState { STOP, UP, DOWN };
// At power-up the mechanism is treated as already being at its UP position.
MotorState lastRequestedPosition = MotorState::UP;
MotorState motorMessageState = MotorState::UP;

void setMotor(MotorState state) {
  // BTS7960 direction is selected by applying PWM to exactly one input.
  // Keeping the other PWM input at zero prevents a conflicting command.
  if (state == MotorState::UP) {
    analogWrite(MOTOR_RPWM_PIN, MOTOR_PWM_50_PERCENT);
    analogWrite(MOTOR_LPWM_PIN, 0);
  } else if (state == MotorState::DOWN) {
    analogWrite(MOTOR_RPWM_PIN, 0);
    analogWrite(MOTOR_LPWM_PIN, MOTOR_PWM_50_PERCENT);
  } else {
    analogWrite(MOTOR_RPWM_PIN, 0);
    analogWrite(MOTOR_LPWM_PIN, 0);
  }
}

void stopMotor() {
  const bool wasMoving = motorMoving;
  setMotor(MotorState::STOP);
  motorMoving = false;
  if (wasMoving) {
    lastMotorOutputChangeMs = millis();
    idleTitleActive = false;
  }
}

void printLine(uint8_t row, const char *text) {
  char padded[17];
  snprintf(padded, sizeof(padded), "%-16.16s", text);
  lcd.setCursor(0, row);
  lcd.print(padded);
}

const char *motorStateName(MotorState state) {
  if (state == MotorState::UP) return "UP";
  if (state == MotorState::DOWN) return "DOWN";
  return "STOP";
}

void startMotorConditionMessage(MotorState state, unsigned long now) {
  motorMessageState = state;
  motorMessageOffset = 0;
  motorMessageStartMs = now;
  motorMessageActive = true;
  lastMotorMessageMs = now - MOTOR_MESSAGE_INTERVAL_MS;
}

void updateMotorConditionMessage(unsigned long now) {
  if (!motorMessageActive) return;
  if (now - motorMessageStartMs >= MOTOR_MESSAGE_DURATION_MS) {
    motorMessageActive = false;
    return;
  }
  if (now - lastMotorMessageMs < MOTOR_MESSAGE_INTERVAL_MS) return;

  lastMotorMessageMs = now;
  const char *message = motorMessageState == MotorState::UP
                            ? "<<< MOTOR CONDITION: UP >>>   "
                            : "<<< MOTOR CONDITION: DOWN >>> ";
  const size_t messageLength = strlen(message);
  char line[17];
  for (uint8_t column = 0; column < 16; ++column) {
    line[column] = message[(motorMessageOffset + column) % messageLength];
  }
  line[16] = '\0';
  printLine(0, line);
  printLine(1, motorMessageState == MotorState::UP ? "Moving up..." : "Moving down...");
  motorMessageOffset = (motorMessageOffset + 1) % messageLength;
}

void startIdleTitle(unsigned long now) {
  idleTitleStartMs = now;
  lastIdleTitleAnimationMs = now - IDLE_TITLE_INTERVAL_MS;
  idleTitleActive = true;
  idleTitleFlashOn = false;
  idleTitleOffset = 0;
}

void updateIdleTitle(unsigned long now) {
  if (!idleTitleActive) return;
  if (now - idleTitleStartMs >= IDLE_TITLE_DURATION_MS) {
    idleTitleActive = false;
    // Start the ten-second quiet period after the three-second title finishes.
    lastMotorOutputChangeMs = now;
    return;
  }
  if (now - lastIdleTitleAnimationMs < IDLE_TITLE_INTERVAL_MS) return;

  lastIdleTitleAnimationMs = now;
  const size_t titleLength = strlen(STARTUP_TITLE);
  char titleLine[17];
  for (uint8_t column = 0; column < 16; ++column) {
    titleLine[column] = STARTUP_TITLE[(idleTitleOffset + column) % titleLength];
  }
  titleLine[16] = '\0';
  printLine(0, titleLine);
  printLine(1, idleTitleFlashOn ? "  System ready *" : "  System ready  ");
  idleTitleOffset = (idleTitleOffset + 1) % titleLength;
  idleTitleFlashOn = !idleTitleFlashOn;
}

void showReadings(float temperatureC, float humidity, MotorState requestedPosition, bool lightDetected) {
  char line1[17];
  char line2[17];
  snprintf(line1, sizeof(line1), "T:%.1fC H:%.0f%%", temperatureC, humidity);
  snprintf(line2, sizeof(line2), "L:%s M:%s", lightDetected ? "BRIGHT" : "DARK", motorStateName(requestedPosition));
  printLine(0, line1);
  printLine(1, line2);
}

void showSensorError(bool lightDetected) {
  printLine(0, "DHT11 read failed");
  printLine(1, lightDetected ? "L:BRIGHT M:STOP" : "L:DARK M:STOP");
}

void updateStartupAnimation(unsigned long now) {
  if (!startupActive || now - lastStartupAnimationMs < STARTUP_ANIMATION_INTERVAL_MS) return;

  lastStartupAnimationMs = now;
  char titleLine[17];
  const size_t titleLength = strlen(STARTUP_TITLE);
  for (uint8_t column = 0; column < 16; ++column) {
    titleLine[column] = STARTUP_TITLE[(startupTitleOffset + column) % titleLength];
  }
  titleLine[16] = '\0';
  printLine(0, titleLine);
  printLine(1, startupFlashOn ? "  Starting... *" : "  Starting...  ");

  startupTitleOffset = (startupTitleOffset + 1) % titleLength;
  startupFlashOn = !startupFlashOn;
}

MotorState chooseRequestedPosition(float temperatureC, float humidity, bool lightDetected) {
  if (temperatureC > 30.0f) return MotorState::DOWN;
  if (temperatureC <= 28.0f) return MotorState::UP;
  if (humidity < 70.0f) return MotorState::DOWN;
  if (humidity >= 75.0f) return MotorState::UP;
  return lightDetected ? MotorState::DOWN : MotorState::UP;
}

void setup() {
  Serial.begin(115200);
  pinMode(MOTOR_RPWM_PIN, OUTPUT);
  pinMode(MOTOR_LPWM_PIN, OUTPUT);
  analogWriteResolution(MOTOR_RPWM_PIN, 8);
  analogWriteResolution(MOTOR_LPWM_PIN, 8);
  analogWriteFrequency(MOTOR_RPWM_PIN, 20000);
  analogWriteFrequency(MOTOR_LPWM_PIN, 20000);
  pinMode(LIGHT_SENSOR_PIN, INPUT);
  stopMotor();

  dht.begin();
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
  lcd.init();
  lcd.backlight();
  lcd.clear();
  startupStartMs = millis();
  lastMotorOutputChangeMs = startupStartMs;
  lastStartupAnimationMs = startupStartMs - STARTUP_ANIMATION_INTERVAL_MS;
  updateStartupAnimation(startupStartMs);
}

void loop() {
  const unsigned long now = millis();

  // This check runs every pass through loop, so a move lasts one second, not one sample period.
  if (motorMoving && now - motorStartMs >= MOTOR_MOVE_TIME_MS) {
    stopMotor();
  }
  updateMotorConditionMessage(now);

  if (startupActive) {
    updateStartupAnimation(now);
    if (now - startupStartMs < STARTUP_DURATION_MS) return;
    startupActive = false;
    // Take the first sensor reading as soon as the three-second title motion ends.
    lastSampleMs = now - SAMPLE_INTERVAL_MS;
  }

  if (!motorMoving && !motorMessageActive && haveValidReading) {
    if (!idleTitleActive && now - lastMotorOutputChangeMs >= IDLE_TITLE_PERIOD_MS) {
      startIdleTitle(now);
    }
    updateIdleTitle(now);
  }

  if (now - lastSampleMs < SAMPLE_INTERVAL_MS) return;
  lastSampleMs = now;

  // Typical LM393 light-sensor modules pull DO LOW at the selected light threshold.
  const bool lightDetected = digitalRead(LIGHT_SENSOR_PIN) == LOW;
  const float humidity = dht.readHumidity();
  const float temperatureC = dht.readTemperature();

  if (isnan(humidity) || isnan(temperatureC)) {
    haveValidReading = false;
    stopMotor();
    Serial.printf("DHT11 read failed; light: %s; motor OFF\n", lightDetected ? "BRIGHT" : "DARK");
    showSensorError(lightDetected);
    return;
  }

  haveValidReading = true;
  const MotorState requestedPosition = chooseRequestedPosition(temperatureC, humidity, lightDetected);

  // At power-up the mechanism is assumed to already be UP. Therefore, the first
  // DOWN request is a real change and moves the motor; an initial UP request does not.
  if (requestedPosition != lastRequestedPosition) {
    lastRequestedPosition = requestedPosition;
    idleTitleActive = false;
    lastMotorOutputChangeMs = now;
    setMotor(requestedPosition);
    motorStartMs = now;
    motorMoving = true;
    startMotorConditionMessage(requestedPosition, now);
    updateMotorConditionMessage(now);
    Serial.printf("Position changed to %s; motor runs for 1 second\n", motorStateName(requestedPosition));
  }

  if (!motorMoving && !motorMessageActive && !idleTitleActive) {
    showReadings(temperatureC, humidity, requestedPosition, lightDetected);
  }
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik