Skip to content

12.Mechanical Design, Machine Design

Summary

This week, we developed an automatic blind control system that allows the blinds to open and close automatically through a convenient user interface.

Group Assignment:

Our team consisted of two members, and we divided the tasks to work more efficiently. My teammate, Yerlan agai, was responsible for the mechanical part of the project — he designed the motor housing in a CAD program, printed it on a 3D printer, and mounted it in place. Meanwhile, I focused on the hardware and software aspects of the system — connecting all electronic components, designing the circuit, writing the control code, and testing the functionality. Working together, we successfully built a fully functional prototype of an automatic blind system that combines mechanical precision with intelligent automation.


  • Yerlan was responsible for the mechanical design and assembly of the motor housing.

Development Process

Hardware

PCB board

To control all the electronics, I used a custom board that I designed and fabricated Electronics production week. The board is based on an ESP32 microcontroller, which offers high performance and built-in Wi-Fi connectivity. This microcontroller can simultaneously control a stepper motor and send or receive data via Wi-Fi, making it an ideal choice for automation projects. On the board, I included dedicated signal pins for connecting the stepper motor driver and other peripheral components. This design provides flexibility and scalability for future improvements to the system.

Stepper motor and driver

  • 28BYJ-48 5V Stepper Motor

  • ULN2003 Step Motor Driver Board

The 28BYJ-48 is a little stepper motor suitable for a large range of practical applications.

Here is its technical specifications (as found in a datasheet):

  • Model 28BYJ-48 – 5V

  • Rated voltage: 5VDC

  • Number of phase: 4

  • Speed variation ratio: 1/64

  • Stride angle: 5.625° /64

  • Frequency: 100Hz

  • DC resistance: 50Ω±7%(25℃)

  • Idle in-traction frequency: > 600Hz

  • Idle out-traction frequency: > 1000Hz

  • In-traction torque: >34.3mN.m(120Hz)

  • Self-positioning torque: >34.3mN.m

  • Friction torque: 600-1200 gf.cm

  • Pull in torque: 300 gf.cm

  • Insulated resistance: >10MΩ(500V)

  • Insulated electricity power: 600VAC/1mA/1s

  • Insulation grade: A

  • Rise in temperature: <40K(120Hz)

  • Noise: <35dB (120Hz, No load, 10cm)

Connection

I connected the stepper motor driver to my custom ESP32 board and then wired the stepper motor to the driver. Since my board does not have a built-in USB programmer, I used a USB-TTL adapter to flash the firmware.

First, I tested whether the motor worked. With a 5V supply, the motor successfully rotated, so I began testing basic movement — forward, backward, and speed control.

Next, I implemented a simple web interface. The ESP32 was programmed to create a Wi-Fi access point, allowing phones or laptops to connect and control the blind actuator through a web page.

However, when I first tested the system, the ESP32 kept rebooting every time it tried to start AP mode. After researching the issue online, I discovered that the problem was caused by insufficient power supply.

To fix this, I used two separate laboratory power supplies, as shown in the photo: one supply powered the ESP32, and the second supply provided 12V for the stepper motors so the system could run correctly without voltage drops.

  • One for the ESP32 board

  • One for the stepper motor (to provide enough torque for opening/closing the blinds)

After powering the system properly, everything worked stable, and I continued writing the final control logic for the web interface.

Software

Web Interface Development for Stepper Motor Control (ESP32 + ESP-IDF)

To control my stepper motor remotely, I developed a lightweight web interface using the ESP-IDF framework. I configured the ESP32 to run in SoftAP mode, which allows it to create its own Wi-Fi network called “ESP32-STEP”. This makes the system fully independent from external routers or internet access.

Using the ESP-IDF HTTP Server library, I implemented a small local server that hosts an HTML page. This page contains two direction buttons and an RPM slider. When the user presses a button or moves the slider, the browser sends POST requests to the ESP32 via two REST API endpoints:

/api/dir — changes the rotation direction

/api/speed — updates the motor RPM

On the ESP32 side, the handlers parse incoming POST data and update shared variables protected by a FreeRTOS mutex. The dedicated stepper-control task then reads these variables and updates the motor speed and direction in real time.

I intentionally wrote the firmware using pure C in ESP-IDF, because after comparing different environments in previous weeks (Arduino IDE, MicroPython, VS Code extensions), ESP-IDF showed the best performance, fastest compilation, and the most stable real-time behavior for stepper control.

I structured the code into logical modules:

Stepper initialization

Direction control

Speed control

HTTP handlers

Web interface delivery

Wi-Fi Access Point setup

This modular structure keeps the code clean and easy to maintain.

Since the ESP32 runs its own Access Point and the web server is fully local, the system works even without internet and is safer — the controller is not exposed to external networks, reducing the risk of unauthorized access.

The final web interface provides:

Direction buttons (Forward and Backward)

A speed slider (0–20 RPM)

Instant response over Wi-Fi

No apps, no cables — only a web browser is required

As a result, I achieved a fast, clean, and reliable wireless control system for my stepper motor using ESP-IDF.

Here is code

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"

#include "driver/gpio.h"
#include "esp_rom_sys.h"

#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "esp_wifi.h"
#include "esp_http_server.h"

#define IN1 GPIO_NUM_13
#define IN2 GPIO_NUM_25
#define IN3 GPIO_NUM_26
#define IN4 GPIO_NUM_33

static const char *TAG = "STEPWEB";

static const uint8_t HALFSEQ[8][4] = {
    {1,0,0,0}, {1,1,0,0}, {0,1,0,0}, {0,1,1,0},
    {0,0,1,0}, {0,0,1,1}, {0,0,0,1}, {1,0,0,1}
};

static const gpio_num_t PINS[4] = {IN1, IN2, IN3, IN4};
static const int STEPS_PER_REV = 4096;

static volatile int  g_dir = 0;    
static volatile float g_rpm = 8.0f;
static SemaphoreHandle_t g_state_mtx;
static int seq_index = 0;

static inline void coils_apply(int idx) {
    idx &= 7;
    for (int i = 0; i < 4; ++i)
        gpio_set_level(PINS[i], HALFSEQ[idx][i]);
}

static inline void coils_release(void) {
    for (int i = 0; i < 4; ++i)
        gpio_set_level(PINS[i], 0);
}

static void stepper_init(void) {
    gpio_config_t io = {
        .mode = GPIO_MODE_OUTPUT,
        .pin_bit_mask = (1ULL<<IN1)|(1ULL<<IN2)|(1ULL<<IN3)|(1ULL<<IN4),
        .pull_down_en = 0,
        .pull_up_en = 0,
        .intr_type = GPIO_INTR_DISABLE
    };
    gpio_config(&io);
    coils_release();
}

static inline uint32_t rpm_to_delay_us(float rpm) {
    if (rpm < 0.01f) rpm = 0.01f;
    double d_us = (60.0 * 1000000.0) / (rpm * (double)STEPS_PER_REV);
    if (d_us < 200.0) d_us = 200.0;
    return (uint32_t)(d_us + 0.5);
}

static void stepper_task(void *arg) {
    float cur_rpm = 0.5f;
    const float acc_rpm_per_s = 20.0f;

    while (1) {
        int dir_local;
        float rpm_target;

        xSemaphoreTake(g_state_mtx, portMAX_DELAY);
        dir_local = g_dir;
        rpm_target = g_rpm;
        xSemaphoreGive(g_state_mtx);

        if (dir_local == 0 || rpm_target <= 0.05f) {
            coils_release();
            vTaskDelay(pdMS_TO_TICKS(10));
            continue;
        }

        uint32_t d_cur = rpm_to_delay_us(cur_rpm);
        float dt = (float)d_cur / 1e6f;
        float delta = acc_rpm_per_s * dt;
        if (cur_rpm < rpm_target) {
            cur_rpm += delta;
            if (cur_rpm > rpm_target) cur_rpm = rpm_target;
        } else if (cur_rpm > rpm_target) {
            cur_rpm -= delta;
            if (cur_rpm < rpm_target) cur_rpm = rpm_target;
        }

        seq_index = (seq_index + (dir_local > 0 ? 1 : -1)) & 7;
        coils_apply(seq_index);

        uint32_t d_us = rpm_to_delay_us(cur_rpm);
        if (d_us >= 2000) vTaskDelay(pdMS_TO_TICKS(d_us / 1000));
        else esp_rom_delay_us(d_us);
    }
}

static void wifi_init_softap(void) {
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_ap();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    wifi_config_t ap_cfg = {0};
    strcpy((char*)ap_cfg.ap.ssid, "ESP32-STEP");
    strcpy((char*)ap_cfg.ap.password, "12345678");
    ap_cfg.ap.channel = 6;
    ap_cfg.ap.max_connection = 4;
    ap_cfg.ap.authmode = WIFI_AUTH_WPA_WPA2_PSK;
    if (strlen((char*)ap_cfg.ap.password) == 0)
        ap_cfg.ap.authmode = WIFI_AUTH_OPEN;

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap_cfg));
    ESP_ERROR_CHECK(esp_wifi_start());

    ESP_LOGI(TAG, "SoftAP запущен: SSID=%s PASS=%s http://192.168.4.1/",
             (char*)ap_cfg.ap.ssid, (char*)ap_cfg.ap.password);
}

static const char INDEX_HTML[] =
"<!doctype html><html><head><meta charset='utf-8'/>"
"<meta name='viewport' content='width=device-width,initial-scale=1'/>"
"<title>ESP32 Stepper</title>"
"<style>body{font-family:sans-serif;margin:24px}"
"button{padding:12px 16px;margin:8px;border-radius:10px;border:0;cursor:pointer}"
"#fwd{background:#e8ffe8}#back{background:#ffe8e8}"
"input[type=range]{width:260px;margin-left:10px}"
"</style></head><body>"
"<h2>ESP32 Stepper Controller</h2>"
"<button id='back'>⟲ Назад</button><button id='fwd'>⟳ Вперёд</button>"
"<p>Скорость (RPM): <input id='rpm' type='range' min='0' max='20' step='0.5' value='8'>"
"<span id='val'>8</span></p>"
"<p>Поставьте 0 чтобы остановить мотор.</p>"
"<script>"
"const rpm=document.getElementById('rpm');const val=document.getElementById('val');"
"rpm.oninput=()=>{val.textContent=rpm.value;"
"fetch('/api/speed',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'rpm='+rpm.value});};"
"document.getElementById('fwd').onclick=()=>fetch('/api/dir',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'dir=1'});"
"document.getElementById('back').onclick=()=>fetch('/api/dir',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'dir=-1'});"
"</script></body></html>";

static esp_err_t root_get_handler(httpd_req_t *req) {
    httpd_resp_set_type(req, "text/html; charset=utf-8");
    return httpd_resp_send(req, INDEX_HTML, HTTPD_RESP_USE_STRLEN);
}

static esp_err_t dir_post_handler(httpd_req_t *req) {
    char buf[32];
    int received = httpd_req_recv(req, buf, sizeof(buf) - 1);
    if (received <= 0) {
        httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "no body");
        return ESP_FAIL;
    }
    buf[received] = '\0';

    int dir = 0;
    if (strstr(buf, "dir=1")) dir = +1;
    else if (strstr(buf, "dir=-1")) dir = -1;

    xSemaphoreTake(g_state_mtx, portMAX_DELAY);
    g_dir = dir;
    xSemaphoreGive(g_state_mtx);

    httpd_resp_set_type(req, "application/json");
    return httpd_resp_sendstr(req, "{\"ok\":true}");
}

static esp_err_t speed_post_handler(httpd_req_t *req) {
    char buf[32];
    int received = httpd_req_recv(req, buf, sizeof(buf) - 1);
    if (received <= 0) {
        httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "no body");
        return ESP_FAIL;
    }
    buf[received] = '\0';

    float rpm = 0.0f;
    char *p = strstr(buf, "rpm=");
    if (p) rpm = strtof(p + 4, NULL);
    if (rpm < 0.0f) rpm = 0.0f;
    if (rpm > 20.0f) rpm = 20.0f;

    xSemaphoreTake(g_state_mtx, portMAX_DELAY);
    g_rpm = rpm;
    if (rpm <= 0.05f) g_dir = 0;
    xSemaphoreGive(g_state_mtx);

    httpd_resp_set_type(req, "application/json");
    return httpd_resp_sendstr(req, "{\"ok\":true}");
}

static httpd_handle_t start_httpd(void) {
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.server_port = 80;

    httpd_handle_t server = NULL;
    if (httpd_start(&server, &config) == ESP_OK) {
        httpd_uri_t root = { .uri="/", .method=HTTP_GET, .handler=root_get_handler };
        httpd_uri_t dir  = { .uri="/api/dir", .method=HTTP_POST, .handler=dir_post_handler };
        httpd_uri_t spd  = { .uri="/api/speed", .method=HTTP_POST, .handler=speed_post_handler };
        httpd_register_uri_handler(server, &root);
        httpd_register_uri_handler(server, &dir);
        httpd_register_uri_handler(server, &spd);
        ESP_LOGI(TAG, "HTTP сервер запущен на порту %d", config.server_port);
    }
    return server;
}
void app_main(void) {
    ESP_ERROR_CHECK(nvs_flash_init());
    wifi_init_softap();

    stepper_init();
    g_state_mtx = xSemaphoreCreateMutex();
    xTaskCreate(stepper_task, "stepper", 4096, NULL, 6, NULL);

    start_httpd();
}

I connected a USB-TTL programmer to my custom ESP32 board and successfully uploaded the firmware. The programmer provides the RX/TX interface needed for flashing, and I used the two push buttons on my board (BOOT and RESET) during programming. After wiring everything correctly and selecting the proper port in the IDE, the code was uploaded without any issues.

After uploading the firmware to my ESP32, I powered the board and enabled SoftAP mode. Once the Wi-Fi network “ESP32-STEP” appeared, I connected to it from my phone.

After the connection was successful, I opened the Safari browser and entered the ESP32’s default IP address:

http://192.168.4.1

Safari immediately loaded my custom web interface, where I could control the stepper motor using direction buttons and an RPM slider. This confirmed that the ESP32 SoftAP, HTTP server, and real-time motor control were all working correctly.

In this way, I developed both the electronics and the web-based control interface for the motorized blinds. This system can be further improved by adding different sensors — for example, a light sensor (photodiode/photocell) that automatically closes the blinds at night and opens them during the day. It could also compare the indoor brightness level and adjust the blinds accordingly to maintain comfortable lighting.

This modular approach allows the system to be easily expanded in the future with more smart-home features.

Conclusion

This week, I successfully designed and implemented a motorized blinds system using my custom ESP32 control board. I developed both the electronics and a local web interface that allows manual control of direction and speed. The project worked reliably, and I identified several opportunities for future improvements, such as adding light sensors for full automation. Overall, this assignment strengthened my skills in embedded programming, networking, and system integration.

Files for download

Code for Actuator Control via Web Interface here.