</>CoderDIY
← Back to list
Article

Build a WiFi-Controlled Smart Curtain with ESP32

Author: CoderDIY··Đọc bằng tiếng Việt
🪟

Off-the-shelf smart curtains usually require replacing the entire track system, at a steep price. With a stepper motor, a GT2 timing belt, and an ESP32, you can retrofit an automated puller onto your existing curtain track — opening curtains with the morning light, closing them on a schedule at night, or controlling them straight from your phone without getting out of bed.

How it works

  1. A stepper motor, fitted with a small pulley, pulls a timing belt connected to the curtain hook — as the motor turns, the belt moves and drags the curtain open or closed.
  2. The ESP32 drives the stepper motor through a driver (A4988 or TMC2209), tracking exactly where the curtain is by counting steps taken from a reference point of “fully closed” or “fully open” (established initially with a limit switch or manual calibration).
  3. The ESP32 runs a small web server that accepts open/close/set-percentage commands from an app or a phone browser, while also supporting a time-based schedule (e.g. open at 6:30am, close at 9:00pm) via NTP sync.
  4. Optionally add a light sensor (LDR) to auto-open the curtain once it’s bright enough outside instead of relying on a fixed time, adapting better across seasons.
<rect x="10" y="20" width="140" height="60" rx="10" class="box" />
<text x="80" y="45" text-anchor="middle" class="label">NTP schedule / LDR /</text>
<text x="80" y="62" text-anchor="middle" class="label">app command</text>

<rect x="170" y="20" width="140" height="60" rx="10" class="box" />
<text x="240" y="45" text-anchor="middle" class="label">Compute steps</text>
<text x="240" y="62" text-anchor="middle" class="label">to move</text>

<rect x="330" y="20" width="140" height="60" rx="10" class="box" />
<text x="400" y="45" text-anchor="middle" class="label">Drive the driver</text>
<text x="400" y="62" text-anchor="middle" class="label">(A4988/TMC2209)</text>

<rect x="490" y="20" width="140" height="60" rx="10" class="box" />
<text x="560" y="45" text-anchor="middle" class="label">Stepper motor turns</text>
<text x="560" y="62" text-anchor="middle" class="label">pulls the curtain</text>

<line x1="150" y1="50" x2="170" y2="50" class="arrow" />
<line x1="310" y1="50" x2="330" y2="50" class="arrow" />
<line x1="470" y1="50" x2="490" y2="50" class="arrow" />

<text x="320" y="130" text-anchor="middle" class="sublabel">Current position tracked by step count, used to compute open/close %</text>
The ESP32 accepts commands from multiple sources (schedule, sensor, app) and converts them into stepper motor steps to reach the target position.

Parts list

  • An ESP32 (with WiFi, for the web server and NTP sync)
  • A NEMA17 stepper motor (strong enough for a medium-sized curtain) and an A4988 or TMC2209 driver
  • A GT2 timing belt and matching pulleys, or a pre-made DIY curtain-puller kit built around a stepper motor
  • An optional light-dependent resistor (LDR) for auto-opening by daylight
  • A 12V power supply for the motor driver, plus a separate 5V/3.3V regulator for the ESP32
  • A mounting bracket/frame to attach the motor to the end of the existing curtain track
<rect x="30" y="60" width="150" height="160" rx="10" class="box" />
<text x="105" y="145" text-anchor="middle" class="label">ESP32</text>

<rect x="240" y="60" width="150" height="90" rx="10" class="box" />
<text x="315" y="110" text-anchor="middle" class="label">A4988 driver</text>

<rect x="450" y="60" width="150" height="90" rx="10" class="box" />
<text x="525" y="110" text-anchor="middle" class="label">Stepper motor</text>
<text x="525" y="128" text-anchor="middle" class="label">NEMA17</text>

<rect x="240" y="180" width="150" height="60" rx="10" class="box" />
<text x="315" y="215" text-anchor="middle" class="label">LDR (optional)</text>

<line x1="180" y1="90" x2="240" y2="90" class="wire" />
<text x="210" y="80" text-anchor="middle" class="sublabel">STEP/DIR</text>

<line x1="390" y1="105" x2="450" y2="105" class="wire" />
<text x="420" y="95" text-anchor="middle" class="sublabel">A/B coils</text>

<line x1="180" y1="210" x2="240" y2="210" class="wire" />
<text x="210" y="200" text-anchor="middle" class="sublabel">A0</text>
The ESP32 sends STEP/DIR signals to the A4988 driver, which directly drives the stepper motor's coils; the LDR (if used) is read on an analog pin.

Sample code

#include <WiFi.h>
#include <WebServer.h>
#include <AccelStepper.h>

const char* WIFI_SSID = "your-wifi";
const char* WIFI_PASS = "your-wifi-password";

#define STEP_PIN 26
#define DIR_PIN 27
#define LDR_PIN 34

AccelStepper stepper(AccelStepper::DRIVER, STEP_PIN, DIR_PIN);
WebServer server(80);

const long STEPS_FULLY_CLOSED = 0;
const long STEPS_FULLY_OPEN = 8000; // calibrate to your actual track length
long currentTarget = STEPS_FULLY_CLOSED;

void moveTo(int percentOpen) {
  percentOpen = constrain(percentOpen, 0, 100);
  currentTarget = map(percentOpen, 0, 100, STEPS_FULLY_CLOSED, STEPS_FULLY_OPEN);
  stepper.moveTo(currentTarget);
}

void handleOpen() { moveTo(100); server.send(200, "text/plain", "Opening curtain"); }
void handleClose() { moveTo(0); server.send(200, "text/plain", "Closing curtain"); }
void handleSetPercent() {
  if (server.hasArg("value")) {
    moveTo(server.arg("value").toInt());
  }
  server.send(200, "text/plain", "OK");
}

void setup() {
  Serial.begin(115200);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  Serial.println(WiFi.localIP());

  stepper.setMaxSpeed(1000);
  stepper.setAcceleration(500);

  server.on("/open", handleOpen);
  server.on("/close", handleClose);
  server.on("/set", handleSetPercent); // e.g. /set?value=50
  server.begin();
}

void loop() {
  server.handleClient();
  stepper.run();

  // Auto-open when it's bright out, if using an LDR (uncomment to enable)
  // int light = analogRead(LDR_PIN);
  // if (light > 2500 && currentTarget != STEPS_FULLY_OPEN) moveTo(100);
}

Common pitfalls

  • No limit switch or initial reference point lets the “fully closed”/“fully open” positions drift over repeated runs due to step skipping — add a limit switch at one end of the track to recalibrate the reference point whenever the curtain hits it.
  • Choosing an underpowered stepper motor relative to the curtain’s weight causes it to skip steps mid-run, throwing off the computed position — pick a NEMA17 with enough torque, or gear it down for extra pulling force if needed.
  • Not setting the driver’s current limit correctly on the A4988/TMC2209 leaves the motor overheating or underpowered — always tune the current-limit trimpot to the motor’s spec before running it under real load.
  • A web server with no authentication lets anyone on the LAN control the curtain — add basic auth or restrict access if the WiFi network isn’t fully trusted.

Extensions

Pair it with the Bluetooth presence detector project to auto-close the curtains when everyone’s left (saving on cooling), add voice control through Home Assistant, or add an outdoor temperature sensor to auto-close the curtains against harsh midday summer sun.

Tags:#esp32#home-automation#stepper-motor

Comments