Project

Motorized Smart Blinds with Stepper Motor and Light Sensor

This is the most mechanical of the projects on the site. The electronics are easy; the mechanical fit between a stepper motor and the existing tilt rod of your blinds is what makes or breaks it. Done well, the blinds you already own open and close on schedule, dim themselves when the sun gets too bright, and disappear into normal use the way good home automation should.

How it works

flowchart TD LDR[Photoresistor
+ 10kΩ divider] -->|ADC| ESP32 NTP[pool.ntp.org] -->|sync| ESP32 Web[Phone] -->|HTTP /open or /close| ESP32 ESP32[[ESP32]] -->|STEP/DIR| Driver[A4988
stepper driver] Driver --> Stepper[NEMA17
stepper motor] Stepper -->|3D-printed coupler| Rod[Tilt rod of blinds] Limit[Microswitch
at fully-open] -->|GPIO with INPUT_PULLUP| ESP32

Control flow. The limit switch protects the mechanism — without it, a runaway stepper would over-tilt and break the blinds.

Bill of materials

  • ESP32 dev board — $7
  • NEMA17 stepper (1.8°, 1.5 A or higher) — $10
  • A4988 or DRV8825 stepper driver — $3
  • 3D-printed coupler (matched to your blinds' tilt rod hex) — $5
  • Photoresistor (LDR) + 10 kΩ resistor — $1
  • Microswitch (limit switch) — $1
  • 12 V / 2 A power supply — $6
  • Mounting bracket (3D-printed or aluminum) — $5

Total around $38 per window. Cheaper than commercial smart blinds even per window, and far cheaper than replacing all the blinds in a house.

Wiring

A4988      ESP32 / Stepper / 12V
VMOT   --- 12V supply (+)
GND    --- 12V GND (and ESP32 GND, common)
VDD    --- 3.3V
GND    --- GND
STEP   --- GPIO 26
DIR    --- GPIO 27
EN     --- GPIO 25 (active LOW)
A1, A2 -- coil A
B1, B2 -- coil B

LDR        ESP32
LDR        connected between GPIO 34 and 3.3V
10kΩ       between GPIO 34 and GND (voltage divider)

Limit switch  ESP32
NO     --- GPIO 32 (INPUT_PULLUP)
COM    --- GND

The 100 µF cap across A4988's VMOT/GND is essential for clean motion. Without it, you'll see step skipping and missed positions over time.

Firmware design

The firmware tracks position in steps from the limit switch. Boot sequence: rotate slowly toward the limit until the switch closes, then back off 50 steps — that's position 0 (fully closed). To open, the firmware rotates a fixed number of steps (calibrated per blind type, typically 800 steps for full open). Intermediate positions are linear.

The schedule is stored in flash and updated via web UI. Default: open at sunrise (computed from latitude + date, no API needed), close at sunset, plus an optional "dim if too bright" rule that partially closes when LDR reads above a threshold.

Mechanical fit (the hard part)

Standard horizontal-blind tilt rods come in two shapes: hex (most common) and round. The 3D-printed coupler matches your specific shape. Measure the tilt rod with calipers; the coupler hole must be a snug fit so it doesn't slip but not so tight that it cracks under stepper torque. Test with a manual rotation before powering up.

The stepper mounts to the window frame with a 3D-printed bracket. The bracket needs to keep the stepper shaft aligned with the tilt rod within ~1 mm — misalignment causes binding. A flexible coupling (small spring, or commercial flex coupler) absorbs slight misalignment.

Going further

  • Cordless: a small Li-Po + boost converter inside the bracket. Battery lasts months because operation is brief; add solar trickle-charging at the window.
  • Multi-zone: one ESP32 per window, all reporting to the same MQTT topic; Home Assistant treats them as a group cover.
  • Wind sensor: close the blinds preemptively if the anemometer detects high wind (good for outdoor / patio blinds).

Key code: main loop

This is the heart of the firmware, taken from the working sketch. The complete file (with config template, library list, and the rest of the helpers) is around 93 lines and is included in the downloadable project package — request it via the form below.

void loop() {
// LDR-based dim: if too bright at midday, partially close
    int ldr = analogRead(LDR_PIN);
    static unsigned long lastCheckMs = 0;
    if (millis() - lastCheckMs > 60000) {
        lastCheckMs = millis();
        struct tm ti;
        if (getLocalTime(&ti) && ti.tm_hour >= 11 && ti.tm_hour <= 15) {
            if (ldr > LDR_BRIGHT && currentPos == STEPS_OPEN) {
                Serial.println("[ldr] very bright, dimming");
                moveTo(STEPS_OPEN / 2);
            }
        }
    }
    delay(100);
}
Project package

Get the complete project package

The article above shows the core firmware and the principles behind it. The complete project package — assembled, tested, and ready to flash — is available by email request. We send it manually, and we read every request.

  • Complete Arduino sketch (.ino) with full error handling
  • List of required libraries with version numbers
  • Printable wiring diagram (PDF)
  • Bill of materials with current part numbers
  • Build guide and troubleshooting tips
  • Configuration template (WiFi, MQTT, etc.)

We send the package by email within 24 hours, usually faster. Free, no spam, no mailing list. Your email is used once, for this reply.

Share your thoughts

Worked with this in production and have a story to share, or disagree with a tradeoff? Email us at support@mybytenest.com — we read everything.