WBlinds icon indicating copy to clipboard operation
WBlinds copied to clipboard

WiFi smart blinds controller for ESP32

logo

License Release

Build Wifi-connected smart blinds using ESP32 and off-the-shelf components for <$50.

🌟 Including...

  • HTTP/REST API
  • MQTT API & client
  • Native Homekit integration
  • Halts on home detection
  • Web UI for controlling/configuring

Table of Contents

  • Setup
    • Hardware
    • Circuit
    • Software
  • APIs
  • Web UI
  • Configuration
  • Development
    • Custom Integrations
  • Appendix
    • Other Files
    • Choosing a Motor

✏️ Setup

Hardware

These are some suggestions for hardware to use. The first link in each row is the product I used for the prototype. All links are non-affiliate and can be found elsewhere if you look around. Prices are estimates including shipping at the time I purchased and may have changed since then; I suggest searching around if so.

Part ~~~~~~~~ Purchase Links ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Notes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ESP32 32S Devkit "V1" (AliExpress, $4)
32S Devkit M1 (Mouser, $8)
NodeMCU-32 (Amazon, $11)
Circuit below wass using the first link. Pin assignments may vary between boards, often the same pins but more/less extra pins, or power pins are in different places. Be sure to check the pinout.
Stepper Driver A4988 (AliExpress, $2)
A4988 (Polulu, $6)
5x A4988 (Amazon, $8)
Other drivers would work too, currently only A4988 is tested (using FastAccelStepper).

Be sure to set the current limit before attaching your motor.
Stepper Motor 1.5A 45Ncm (AliExpress, $18)
1.5A 42Ncm (StepperOnline, $5 at time of writing)1.7A 36Ncm (Polulu, $18)
1.5A 42Ncm (Amazon, $10)
This may be over/underpowered for your needs, depending on the weight of the blinds.

See choosing a motor.

Note: The newest 3d designs use a worm gear with a 30:1 reductions so a much smaller motor will work, but the movement speed is greatly reduced with that design.
3.3V Step-down Regulator 5-40V to 3.3V (AliExpress, $2)
D24V5Fx (Polulu, $5)
10x 5-12V to 3.3v (Amazon, $9)
Circuit below is using the one from AliExpress. Others may have different number of pins or pin orders, so check the pinout before wiring.
Power supply 24V 5A (AliExpress, $13)
12V 5A (Polulu, $19)
12V 5A (Amazon, $10)
You can (and probably want to) use a voltage over the motor's rating. That's fine, you just need to stay below it's rated current.

The current needed depends on motor rating and how many blinds you'll be moving simultaneously with the same power supply.
DC barrel jack 5.5mm x 2.1mm (AliExpress, $3)
Microswitch 5x (AliExpress, $2)
1x (Polulu, $1)
6x (Amazon, $6)
Optional, used as a limit switch for the "fully open" position. Any switch could work here.
Blinds HOPPVALS (IKEA, $20-45)
TRIPPEVALS (IKEA, $40-60)
My builds use cellular shades from IKEA, modified to wrap the cord around a 3D printed axis, instead of the retail spring mechanism.

Any blinds that can be moved by wrapping a cord around an axis could easily be adapted. Most cordless blinds could be used with some modifications. Roller blinds would be the easiest in most cases.

Circuit

Schematic files are located in the pcb/ directory. Currently there is a devkit that can be fabricated that fits header pins and will use the following components. Check the parts list carefully, only the first item in each section will fit, since they are what I ordered, but other parts may not. The gerber output is a 2 layer board created with JLCPCB's CAM generator.

  • 30 pin ESP32 devkit
  • 3 pin buck converter (middle pin GND)
  • 3 pin DC barrel jack

You'll also want some pin headers to solder on, both male and female. The parts are throughhole and the gerber has drill holes so you can skip them if you'd like, but I'd recommend at least making the A4988 removable in case you pop one.

Schematic

schematic

Devkit

devkit
  • Remember to adjust the current limit of the A4988 BEFORE connecting it to the circuit -- keep the magic smoke inside.
  • The pins can be set via the web UI, feel free to use different ones.
  • Be sure not to apply 12/24V to your ESP or Vc (or VDD) of the driver IC. It only goes to Vm to power the motor, everything else can use 3.3v.
  • A4988's RST and SLP can be connected to each other if you don't care about the ability to sleep the motor.

3D Parts

Included in the /3d directory are 2 revisions of printable designs for WBlinds, crafted specifically for Ikea HOPPVALS. The same 2 mechanisms could be reworked for any other blinds. r1 has the added benefit of a worm gear mechanism, so power doesn't need to be constantly applied to keep the blinds in place, even with heavy materials.

Revision 1

These are screenshots of the design to give you an idea of how parts fit together. It may be useful to put a damper (Amazon | AliExpress) between the bracket and the stepper to cut down on vibration & noise.

Default

r1-frontr1-leftr1-backr1-right

The 3d/r1/cam/ contains an experimental (and not great) barrel cam mechanism to guide the string along an alternating path around the center spool, functionally similar to what a fishing reel does with a string guide. This can be sticky with 3D-printed parts, and with a good placement of string-guide.stls, it isn't necessary to keep the spooling even.

With optional cam exposed

r1-with-cam

Software

  1. Connect ESP32 to your computer via USB
  2. git clone https://github.com/maxakuru/WBlinds.git
  3. cd WBlinds && yarn
  4. yarn build:cpp
  5. yarn flash
  6. On first boot an access point will be started, connect to it:
    • SSID: WBlinds-XXXXXX
    • Default password: Wbl1nds-1337
  7. After connecting, a webpage should open to configure settings. If not, go to http://4.3.2.1/settings?tab=gen
  8. Configure SSID, password, pins, etc. and tap "SAVE"
  9. You will be disconnected from the AP. Find the IP address of the device on your network and enter it in your browser.

Steps 3-5 assume you have PlatformIO, Node.js, and Yarn installed. You can also flash without Node/Yarn by using the PlatformIO VSCode Plugin. You can also use the scripts to do other stuff.

🔌 APIs

These APIs are in flux!

HTTP

PUT <host:port>/api/state
// All fields optional
{ 
    "tPos": 50, //[0-100] (target position %, starts move)
    "pos": 50, // [0-100] (current position %)
    "accel": 99999, // [0-UINT32_MAX] (steps/s/s)
    "speed": 1000, // [0-something reasonable] (Hz)
}
GET <host:port>/api/state
// 200
{ 
    "tPos": 50,
    "pos": 50,
    "accel": 99999,
    "speed": 1000
}
POST <host:port> 
{ 
    "op": "up" | "down" | "stop" | "sleep"
}
PUT <host:port>/api/settings
// All fields optional
// NOTE: data is sent in body as JSON. If that isn't acceptable for your use, set sensitive fields at compile time.
{ 
    gen: { 
        deviceName: "WBlinds", 
        mdnsName: "WBlinds", 
        emitSync: true,
        ssid: "My Network",
        pass: "supersecurepassword"
    },
    hw: {
        pStep: 19,
        pDir: 18,
        pEn: 23,
        pSleep: 21,
        pReset: 3,
        pMs1: 1,
        pMs2: 5,
        pMs3: 17,
        pHome: 4,
        cLen: 1650,
        cDia: 0.1,
        axDia: 15,
        stepsPerRev: 200,
        res: 16,
    },
    mqtt: {
        enabled: true,
        host: "192.168.1.99",
        port: 1883,
        topic: "WBlinds",
        user: "max",
        pass: "mysupersecurepassword"
    }
}
GET <host:port>/api/settings
{ 
    gen: { 
        deviceName: "WBlinds", 
        mdnsName: "WBlinds",
        ssid: "Some SSID",
        mac: "abcdef123456",
        emitSync: true
    },
    hw: {
        pStep: 19,
        ...
        res: 16
    },
    mqtt: {
        enabled: true,
        host: "192.168.1.99",
        port: 1883,
        topic: "WBlinds",
        user: "max"
    }
}
POST <host:port>/api/restore
// 202, ESP resets once completed
// This wipes Homekit config & state files

MQTT

WBlinds subscribes to a wildcard based on the topic provided. For example, a topic name blinds/living_room will subscribe to blinds/living_room/#.

The topic defines a number of actions without any payload, for example:

topic="blinds/living_room/up"
topic="blinds/living_room/down"
topic="blinds/living_room/stop"
topic="blinds/living_room/sleep"

Some topics can specify additional data:

topic="blinds/living_room/move"
body="{\"pos\":50,\"speed\":1000,\"accel\":1000}"

Where pos is %, speed is Hz, and accel is steps/s^2.

Homekit

Native Homekit integration allows the ESP32 to be detected as a device without a bridge/emulator. This mode requires percentage-based position values, so it's important to calibrate the settings first.

The default Homekit pin is 111-22-333, and it will be correctly identified as a "window covering".

💻 Web UI

Home

Settings

🔧 Configuration

There is a Calibrate button at $HOST/settings?tab=gen that will guide you through a 2 step calibration to find the correct bottom and top positions for the blinds on first setup. These settings will be persisted across power cycles and flashes of new firmwares, as long as you use the same partition.

Default Pins

Pin Use Description
17 pDir Controls direction (optional)
5 pStep Controls steps via PWM with A4988 (required)
18 pSlp Allows putting motor to sleep to save most power possible. (optional)
23 pEn Allows enabling/disabling motor with A4988. By default the motor is disabled and enabled between uses which lowers power consumption. (optional)
19 pRst Reset pin (optional)
1 pMs1 Microstep resolution pin 1, using microsteps can decrease sound (optional)
3 pMs2 Microstep resolution pin 2 (optional)
21 pMs3 Microstep resolution pin 3 (optional)
4 pHomeSw Home trigger switch, recommended as a way to hard stop when the blinds reach the fully contracted position to avoid damaging hardware when steps are skipped or malfunctions. (optional)

📝 Development

There are 2 projects that make up this repo: the C++ ESP controller and a TypeScript-based web UI. The web UI is transpiled to Javascript, inlined into HTML, then gzipped/chunked into header files with a script. All generated header files as well as transpiled JS is included in the repo, so no additional build steps are needed.

Scripts

These can be run with npm or yarn, I'm currently using yarn. None of these are required to build and flash the ESP bin, as mentioned above all the files are checked into git, so you can compile the C part however you like.

However, these scripts are used for pre-commit hooks, so if you want to contribute you'll need Node.js.

UI

Build web UI in development/watch mode (outputs to public/):

yarn dev

Build web UI minified (outputs to public/, also runs build:uih afterwards):

yarn build:ui

Only generate header files from public/:

yarn build:uih

ESP

The following scripts depend on PlatformIO, and can be replaced with the toolbar quick actions in VSCode if you prefer.

Compile C++ (assumes *nix machine right now):

yarn build:cpp

Build & flash using pio:

yarn flash

Flash over-the-air (requires an initial flash while plugged in and WiFi connection established through settings):

yarn flash:ota $IP_OR_HOSTNAME

Both

You can combine all build scripts with:

yarn build

Tools

I recommend using VSCode to build/flash. Eventually I plan to cut releases of precompiled binary that can be flashed directly, but for the time being you'll need to pull the code and compile it yourself. That is made very simple by the wonderful PlatformIO - I highly recommend using it with their VSCode plugin.

There are some other suggested plugins in the .vscode directory; VSCode should suggest installing any missing plugin on first boot, but none are required.

Custom integrations

Additional integrations can be built in by extending the WBlindsObserver class and attaching to it, specifying the event flags you are interested in.

#include "defines.h"
#include "state.h"
#include "event.h"
#include <SpecialLib.h>

class SpecialLibIntegration : protected WBlindsObserver {
public:
    explicit SpecialLibIntegration() {
        EventFlags flags;
        flags.pos_ = true;
        flags.speed_ = true;
        flags.accel_ = true;
        flags.targetPos_ = true;
        // ... any other flags you're interested in
        State::getInstance()->Attach(this, flags);
    };
    ~SpecialLibIntegration() override {
        specialLib.teardown();
        State::getInstance()->Detach(this);
    };
    void SpecialLibIntegration::handleEvent(const WBlindsEvent& event) {
        // ... do something with the event
        auto state = State::getInstance();
        if(event.flags_.pos) {
            int pos = state.getPosition();
            specialLib.doStuffWithPosition(pos);
        }
     };
}

💤 Appendix

Other files

The /3d directory contains some STL files that may be useful for building WBlinds. Those are designed for use with IKEA HOPPVALS, but would be close to the TRIPPVALS, too. There are 2 revisions:

  1. r0 uses a direct drive mechanism with a center axle along the entire width of the blind bracket.
  2. r1 uses a worm gear and single spool in the center of the blinds bracket. This won't work with in window mounting.

Note: Like everything else, those designs are a WIP :)

Choosing a motor

TL;DR: putting it together.

Out of the several variables a stepper motor has I've narrowed it down to the most important for our purposes. It's worth mentioning that WBlinds could be built using a regular DC motor and an encoder. Stepper motors are cheap and widely available, largely thanks to 3D printing. They also simplify the design quite a bit, so they're what I will focus on.

Skipped steps

A skipped step happens most often when:

  1. The motor isn't provided with enough volts/amps to move the load.
  2. The load is heavier than the stepper's holding torque.
  3. Power is switched off, the motor is "released", and the load is greater than the motor's detent torque.
  4. Resonance/vibrations.

What skipped steps means to us is that the current state the motor "thinks" it's at is actually off by the number of steps skipped. This is especially bad if you don't have a limit switch to avoid damage, but also just annoying for the end user.

#1 & #2 can be avoided by choosing a proper motor, power supply, and current limit for your stepper driver.

#3 can be avoided by using a high gear ratio, a brake, or a homing switch and rehoming on reset.

#4 is tricky to identify and to fix (you'll probably be fine ¯\_(ツ)_/¯ ).

Gear ratio

Not all stepper motors will have gears, but many do. Some also have a gearbox that increases the gear ratio. There are different kinds of gearboxes: planetary, worm, spur. Then there are different shapes of gears.. different gear profiles.. different tooth pitches etc.

For this guide we'll just think about gear ratios and planetary stepper motors.

Higher gear ratio benefits:

  • Higher torque for less amperage
  • Lower noise
  • High holding torque (less drift when it's off)
  • Fewer skipped steps (both from torque and resonance)

Drawbacks:

  • Higher cost ($30+, or ~3-5x)
  • Lower speed
  • Often large or awkward shapes (for our purpose)

Considering our speed requirements are pretty low (or at least mine are), you could reasonably go with either. If you have large/heavy blinds it may make sense to look for a planetary stepper. I personally have been using regular NEMA17 steppers, but that's because I already had them.

Note: There's also a difference between permanent magnet (PM) planetary steppers and "hybrids".

If you find a good, inexpensive planetary stepper to include in the parts list, contact me!

Speed/Torque

Whether you choose a regular stepper or gearbox stepper, the same final choices come up: how fast it moves vs. how much weight it can move.

As speed increases torque (generally) decreases, you can see this in the torque curves of a regular stepper motor and planetary stepper.

You want to find a motor that has a high enough speed that you aren't waiting until sunset for your blinds to open, and a high enough torque that you aren't skipping steps or stalling at 90% open.

Putting it together

This won't be exact, but is a good enough estimate. I'll use the defaults from the included STLs and IKEA TRIPPEVALS, along with a constant speed. In practice, torque will be higher at the moment it starts to move, as acceleration is applied to go from stopped to max speed. I'll ignore it since the acceleration will be very low (as will top speed).

This is assuming a setup where a cord (A) or fabric (B) wraps around an axis that is directly driven by the motor.. something like these:

# A
|```````|   __________________
| Motor |==|__||||______||||__| <- cord spooled (ie. TRIPPEVALS)
|_______|  |   ||        ||   |
           |   ||        ||   |
           |___||________||___|

# B
|```````|   __________________
| Motor |==|__________________| <- roll of fabric (ie. roller blinds)
|_______|  |                  |
           |  Blocking window |
           |__________________|
  1. Get some variables:
# constants
g = 9.8m/s^2

# The thicker the axis, the fewer rotations needed and faster it can move, at the cost of higher torque required.
Axis diameter   (Da) = 15mm = 1.5cm

# Thickness of cord or fabric, since Da increases as the cord is wrapped around.
Cord diameter   (Dc) = 0.5mm = 0.05cm

# Roughly length from top to bottom of window.
Cord Length     (Lc) = 1650m

# You can use a luggage scale to get a rough estimate, or just guesstimate conservatively.
Maximum load    (m)  = 5kg 

# Range of acceptable durations to go from fully opened to fully closed or vice versa. The faster it moves the louder it will be.
Slowest         (t-) = 3min
Fastest         (t+) = 1min
  1. Calculate revolutions to fully wrapped:

See handy calculator here

         Dc - Da + sqrt[(Da - Dc)^2 + (4 * Dc * Lc) / pi]
Nrev = ------------------------------------------------
                      2 * Dc
Nrev = (1 - 15 + sqrt((15 - 1)^2 + (4 * 0.5 * 1650) / 3.14)) / (2 * 0.5)
Nrev ~= 21
  1. Calculate acceptable RPM range:
RPM = Nrev / t
RPM- = 21 / 3 = 7
RPM+ = 21 / 1 = 21
  1. Calculate peak torque:
# Some worst-case fudge here, in this example it barely impacts the result.
T = m * g * ((Nrev * Dc + Da) / 2)
T = 5 * 9.8 * ((30 * 0.0005 + 0.0015) / 2)
T = 0.404 N.m ~= 40N.cm

So, a motor that can pull 40N.cm at around 7 to 21 RPM is good for this example. Now you can browse motors and look at torque curves to find a suitable choice, like this $10 stepper (torque curve) or this $24 planetary (torque curve).