ESPHome - 28BYJ-48 motors with A4988 driver (Working Solution)

I recommend you watching this:

Motorize and Automate your Blinds for $10! (WiFi) - YouTube specially at 2:30

Ok this one tested my whole knowledge of electrical engineering :grinning:

I was under the impression that a 12V 28BYJ-48 will have more torque than the 5V version but seems like I may be better off using a 12V supply on a 5V version with A4988 driver in short burst. And this is actually true as the motor shaft literally need move just about 90 degrees (1/4th rotation) to get blinds to either open or close.

I will give this one a try and report updates!!

Another question, is it fair to assume that barring any manufacturing quality variances all 5V 28BYJ-48 should have same torque?

Just trying to eliminate OEM specific factor since I searched for the the cheapest deal on Amazon/eBay/Ali and ordered.

From all information that I have read/watch on the 28BYJ-48, I would say that all behave the same. I have used different brands on different projects and they are the same. Of course, that’s a generalization. :slight_smile:

If you don’t have this fixed by now I would suggest limiting the stepper speed to 300 steps and setting your accel and decell to arounld 150.

1 Like

Hiya,

I’m trying to do exactly the same thing. Here’s where I ended up:

  • Hardware: D1Mini (ESP8266), A4988 motor driver, 28BYJ-48 motor (5V version, hacked to be bipolar), voltage converter (set to convert 12V to 5V), 12V power supply.
  • Mounting: I don’t have a 3D printer but I LOVE instamorph and use it for everything, and it’s perfect for this. You can get knockoff versions, they’re basically the same. Heat it up in a pot of hot water, and you get a gob of almost molten plastic, into which you can sink the motor to make a mounting. It hardens solid in < 5min and I found it was completely perfect.
  • Coupling: I have two blinds and they required different couplings because one has a very stiff tilt shaft, the other moves easily
    • For the easy moving one, I used a metal shaft coupler from Amazon. Not perfect as I had to grind down the tilt rod slightly but very strong.
    • For the stiff one, I left the worm gear mechanism in place and just removed the spool, replacing it with a LEGO 24-tooth Technic gear. I fitted a 16-tooth gear to the motor shaft, and mounted the motor sideways so that the gears meshed. This gear ratio plus the worm gear mechanism gave me enough reduction to allow the 28BYJ to tilt these very stiff blinds.
  • Connections (sorry I don’t know how to do the fancy diagrams!):
    • Control
      • D1Mini D2 — A4988 Sleep — A4988 Reset (see below!)
      • D1Mini D3 — A4933 Step
      • D1Mini D4 — A4988 Direction
    • Motor
      • A4988 1B pin 11 — Motor orange
      • A4988 1A pin 12 — Motor pink
      • A4988 2A pin 13 — Motor yellow
      • A4988 2B pin 14 — Motor blue
    • Motor power
      • 12VAC — Voltage converter IN+ — A4988 VMOT
      • Voltage converter IN- — A4988 GND pin 15
    • Logic power
      • Voltage converter OUT+ (5V) — D1Mini 5V — A4988 VDD
      • D1Mini GND — A4988 GND pin 9 — Voltage converter OUT-
  • ESPHome config:
    esphome:
      name: blinds-tilt3
      platform: ESP8266
      board: d1_mini
      esp8266_restore_from_flash: true
      on_boot:
        priority: -100
        then:
          - stepper.report_position:
              id: blind_stepper
              position: !lambda "return id(saved_position);"
          - stepper.set_target:
              id: blind_stepper
              target: !lambda "return id(saved_position);"
          - stepper.set_speed:
              id: blind_stepper
              speed: 200 steps/s
          - script.execute: update_cover_position
    
    logger:
    
    ota:
      password: "foo" # set by ESPHome - use that value!
    
    wifi:
      ssid: "foo"
      password: "foo"
      reboot_timeout: 2min
    
      # Enable fallback hotspot (captive portal) in case wifi connection fails
      ap:
        ssid: "Blinds-Tilt3 Fallback Hotspot"
        password: "foo"
    
    captive_portal:
    
    
    # Enable Home Assistant API
    api:
      services:
        - service: set_stepper_target
          variables:
            target: int
          then:
            - stepper.set_target:
                id: blind_stepper
                target: !lambda 'return target;'
            - script.execute: record_stepper_position
        - service: set_stepper_speed
          variables:
            speed: int
          then:
            - stepper.set_speed:
                id: blind_stepper
                speed: !lambda 'return speed;'
        - service: set_stepper_position
          variables:
            stepper_position: int
          then:
            - stepper.report_position:
                id: blind_stepper
                position: !lambda "return stepper_position;"
            - stepper.set_target:
                id: blind_stepper
                target: !lambda "return stepper_position;"
    
    globals:
      - id: open_position
        type: int
        initial_value: '8000'
      - id: closed_position
        type: int
        initial_value: '1000'
      - id: saved_position
        type: int
        initial_value: '1000'
        restore_value: true
    
    stepper:
      - platform: a4988
        id: blind_stepper
        dir_pin: D4
        step_pin: D3
        sleep_pin: D2
        max_speed: 250 steps/s
        acceleration: 200
        deceleration: 200
    
    cover:
      - platform: template
        id: "blind_cover"
        device_class: blind
        name: "Blinds"
        has_position: true
        optimistic: false
        open_action:
          - logger.log: "Opening"
          - cover.template.publish:
              id: blind_cover
              current_operation: OPENING
          - stepper.set_target:
              id: blind_stepper
              target: !lambda "return id(open_position);"
          - while:
              condition:
                lambda: 'return id(blind_stepper).current_position < id(open_position);'
              then:
                - script.execute: update_cover_position
                - delay: 1000 ms
          - script.execute: update_cover_position
          - script.execute: record_stepper_position
          - cover.template.publish:
              id: blind_cover
              current_operation: IDLE
        close_action:
          - logger.log: "Closing"
          - cover.template.publish:
              id: blind_cover
              current_operation: CLOSING
          - stepper.set_target:
              id: blind_stepper
              target: !lambda "return id(closed_position);"
          - while:
              condition:
                lambda: 'return id(blind_stepper).current_position > id(closed_position);'
              then:
                - script.execute: update_cover_position
                - delay: 1000 ms
          - script.execute: update_cover_position
          - script.execute: record_stepper_position
          - cover.template.publish:
              id: blind_cover
              current_operation: IDLE
        position_action:
          - logger.log: "Setting position"
          - stepper.set_target:
              id: blind_stepper
              target: !lambda 'return (float(pos) * float( float(id(open_position)) - float(id(closed_position)) )) + float(id(closed_position));'
          - while:
              condition:
                lambda: 'return id(blind_stepper).current_position != ((float(pos) * float( float(id(open_position)) - float(id(closed_position)) )) + float(id(closed_position)));'
              then:
                - script.execute: update_cover_position
                - delay: 1000 ms
          - script.execute: update_cover_position
          - script.execute: record_stepper_position
          - cover.template.publish:
              id: blind_cover
              current_operation: IDLE
        stop_action:
          - logger.log: "Stopping"
          - cover.template.publish:
              id: blind_cover
              current_operation: IDLE
          - stepper.set_target:
              id: blind_stepper
              target: !lambda 'return id(blind_stepper).current_position;'
          - script.execute: update_cover_position
          - script.execute: record_stepper_position
    
    sensor:
      - platform: template
        name: "Current stepper position"
        lambda: return id(blind_stepper).current_position;
        update_interval: 5s
    
    script:
      - id: update_cover_position
        then:
          - cover.template.publish:
              id: blind_cover
              position: !lambda 'return float( float(id(blind_stepper).current_position) - float(id(closed_position))) / float( float(id(open_position)) - float(id(closed_position)) );'
      - id: record_stepper_position
        then:
          - globals.set:
              id: saved_position
              value: !lambda 'return id(blind_stepper).current_position;'
    

Key learnings:

  • Many tutorials tell you to bridge the reset and sleep pins but don’t say to provide a sleep signal. I tried various combinations before realising that I needed to connect a pin on my microcontroller to both the sleep and reset pins, and configure it in ESPHome as the sleep pin.
  • D4 is the built in LED on the D1 Mini so if you use it for the direction signal to the motor controller, it will light up when the motor is going in one direction and be off when it goes in the other direction!
  • It’s pretty vital to have the ESPHome remember the current position of the motor, because if not, and you have a power cut, when the power comes back you might break your blinds/motor/mount by trying to close already-closed blinds.
  • Having a bit of acceleration and deceleration in the ESPHome config gives you extra torque when you need it.
10 Likes

Hi, how do I get it to turn the other way? And also how do I control the home assistant? Can you help me?

@triblondon Thanks Andrew, your step by step instructions worked out of the box!

@fourtrax011 applying your tweak ( 300 stepper speed and 150 for acceleration/decceleration) did boost the torque and blinds moved :sunglasses:

HI Triblondon,

thnx for sharing your code,
i still got problems, current position value randomly “freaks”. been banging my head for days…
first some questions:
by globals: restore value is set to TRUE, makes it look at the stored value in “saved_position”"

  • so if i wan’t to calibrate i set this to FALSE ?
    meaning: mij blind is up (stepper=0), set the global saved position to 0 and restore_value: FALSE.
    then i do an upload (OTA) and i see op the webpage that the position is set to ). -ALL good so far.
    then i go back to edit and make this value TRUE.
    again an upload and the webpage is still saying position : 0.

now i do some movements, wait some seconds, make another movement that brings the blind back to 0.
now i wait again some seconds and do a restart…
instead of getting the position zero i get a totally different readout…
here’s “my” code…

esphome:
  name: blind2
  on_boot:
    priority: -500.0
    then:
      - stepper.report_position:
          id: blind_stepper
          position: !lambda "return id(saved_position);"
      - stepper.set_target:
          id: blind_stepper
          target: !lambda "return id(saved_position);"
      - stepper.set_speed:
          id: blind_stepper
          speed: 450 steps/s
      - script.execute: update_cover_position

  on_shutdown:
    then:
      - delay: 10ms
      - script.execute: record_stepper_position
      - delay: 10 ms


esp8266:
  board: esp01_1m
  restore_from_flash: true

globals:
  - id: open_position               # verschil tussen open en dicht = 28.800
    type: int
    initial_value: '0'
  - id: closed_position
    type: int
    initial_value: '28800'
  - id: saved_position
    type: int
    initial_value: '28800'
    restore_value: true
ota:
  
  
  

wifi:
  ssid: blablabla
  password: zegikniet
  power_save_mode: none
  fast_connect: true
  reboot_timeout: 5s
  #enable_mdns: true
  domain: .local



  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Blind2"
    password: "zegiknogsteedsniet"
    
    
 

captive_portal:

# Enable logging
logger:
 level: very_verbose

# Enable Web server
web_server:
  port: 80
  
time:
  - platform: homeassistant
    id: homeassistant_time  

# Enable Home Assistant API
 ####### 
api:
  services:
    - service: control_stepper                        #  set_stepper_target
      variables:
        target: int
      then:
        - stepper.set_target:
            id: blind_stepper
            target: !lambda 'return target;'
        - script.execute: record_stepper_position
    - service: set_stepper_speed
      variables:
        speed: int
      then:
        - stepper.set_speed:
            id: blind_stepper
            speed: !lambda 'return speed;'
    - service: set_stepper_position
      variables:
        stepper_position: int
      then:
        - stepper.report_position:
            id: blind_stepper
            position: !lambda "return stepper_position;"
        - stepper.set_target:
            id: blind_stepper
            target: !lambda "return stepper_position;"
            
    - service: control_servo2
      variables:
        level: float
      then:
        - servo.write:
            id: servo2
            level: !lambda 'return level / 1.0;' 
        

cover:

  - platform: template
    id: "blind_cover"
    device_class: blind
    name: "Blinder2"
    has_position: true
    optimistic: false
    open_action:
      - logger.log: "Opening"
      - cover.template.publish:
          id: blind_cover
          current_operation: OPENING
      - stepper.set_target:
          id: blind_stepper
          target: !lambda "return id(open_position);"
      - while:
          condition:
            lambda: 'return id(blind_stepper).current_position < id(open_position);'
          then:
            - script.execute: update_cover_position
            - delay: 1000 ms
      - script.execute: update_cover_position
      - script.execute: record_stepper_position
      - cover.template.publish:
          id: blind_cover
          current_operation: IDLE
    close_action:
      - logger.log: "Closing"
      - cover.template.publish:
          id: blind_cover
          current_operation: CLOSING
      - stepper.set_target:
          id: blind_stepper
          target: !lambda "return id(closed_position);"
      - while:
          condition:
            lambda: 'return id(blind_stepper).current_position > id(closed_position);'
          then:
            - script.execute: update_cover_position
            - delay: 1000 ms
      - script.execute: update_cover_position
      - script.execute: record_stepper_position
      - cover.template.publish:
          id: blind_cover
          current_operation: IDLE
    position_action:
      - logger.log: "Setting position"
      - stepper.set_target:
          id: blind_stepper
          target: !lambda 'return (float(pos) * float( float(id(open_position)) - float(id(closed_position)) )) + float(id(closed_position));'
      - while:
          condition:
            lambda: 'return id(blind_stepper).current_position != ((float(pos) * float( float(id(open_position)) - float(id(closed_position)) )) + float(id(closed_position)));'
          then:
            - script.execute: update_cover_position
            - delay: 1000 ms
      - script.execute: update_cover_position
      - script.execute: record_stepper_position
      - cover.template.publish:
          id: blind_cover
          current_operation: IDLE
    stop_action:
      - logger.log: "Stopping"
      - cover.template.publish:
          id: blind_cover
          current_operation: IDLE
      - stepper.set_target:
          id: blind_stepper
          target: !lambda 'return id(blind_stepper).current_position;'
      - script.execute: update_cover_position
      - script.execute: record_stepper_position


script:
  - id: update_cover_position
    then:
      - cover.template.publish:
          id: blind_cover
          position: !lambda 'return float( float(id(blind_stepper).current_position) - float(id(closed_position))) / float( float(id(open_position)) - float(id(closed_position)) );'
  - id: record_stepper_position
    then:
      - globals.set:
          id: saved_position
          value: !lambda 'return id(blind_stepper).current_position;'
# Example configuration entry


# Example output platform

output:
  - platform: esp8266_pwm
    id: pwm_output
    pin: GPIO2
    frequency: 50 Hz
    

  - platform: esp8266_pwm
    pin: GPIO3
    frequency: 19000 Hz
    id: light_pwm_output

servo:
  - id: servo2
    output: pwm_output
    #restore: true 
    auto_detach_time: 400ms 
    restore: true

stepper:
  - platform: uln2003
    id: blind_stepper
    sleep_when_done: true
    pin_a: GPIO4
    pin_b: GPIO16
    pin_c: GPIO14
    pin_d: GPIO12
    max_speed: 450 steps/s
    step_mode: FULL_STEP
    # Optional:
    acceleration: inf
    deceleration: inf

sensor:

  - platform: template
    name: "Current stepper2 position"
    lambda: return id(blind_stepper).current_position;
    update_interval: 5s

  - platform: wifi_signal
    name: "blinder2 WiFi Signal"
    update_interval: 60s 
      
# Extra sensor to keep track of plug uptime
  - platform: uptime
    name: blinder2_Uptime Sensor


light:
  - platform: monochromatic
    output: light_pwm_output
    name: "light blind2"



    
switch:

  
  - platform: restart
    name: "blind2 Restart"
    id: restart_switch   


This is not the prettiest diagram but hopefully helps visualize the circuit and connections!

Hello,

I see in the solution, you use a 5V version of the 28BYJ-48 steooer motor with a 12V VMOT through the A4988, is there a risk to burn the motor?

Initially I had only 5V motor which worked fine as the load was in short bursts, but I replaced it with 12V motor eventually.

1 Like

Did you ever figure this out? I’m experiencing the same thing where the stepper only turns one direction.

I’ve got mine working today, same hardware. But it doesn’t quite pull hard enough at the end to fully close the tilt. How do you suggest setting up the acceleration for extra torque I require?

I was able to increase torque using two things:

  1. Opened the blind and applied WD40 to the pulley, it reduced friction significantly.
  2. Played around stepper increment settings and finally the code below is the one that made blinds turned.
#The substitution block has all the configurable details
substitutions:
  name_of_board: blinds01
  ota_password: !secret blinds01_ota
  ap_point: !secret blinds01_ssid
  ap_point_password: !secret blinds01_ssid_password
  #api_password: !secret blinds01_api_password
  api_encryption_key: !secret blinds01_api_encryption
  stepper_speed: "300 steps/s"
  stepper_max_speed: "250 steps/s"
  acceleration_value: "150"
  deceleration_value: "150"
  initial_open_position: "8000" # sets this as the open position or the farthest motor will move for open
  initial_closed_position: "1000" # sets this as the close position or the farthest motor will move for close
  initial_saved_position: "1000" # sets the default value if there is no saved position saved to flash

############################################
#YOU SHOULD NOT NEED TO EDIT BELOW THIS LINE
############################################
esphome:
  name: $name_of_board
  platform: ESP8266
  board: d1_mini
  esp8266_restore_from_flash: true
  on_boot:
    priority: -100
    then:
      - stepper.report_position:
          id: blind_stepper
          position: !lambda "return id(saved_position);"
      - stepper.set_target:
          id: blind_stepper
          target: !lambda "return id(saved_position);"
      - stepper.set_speed:
          id: blind_stepper
          speed: $stepper_speed
      - script.execute: update_cover_position

# Enable logging
logger:


ota:
  password: $ota_password

wifi:
  ssid: !secret ssid
  password: !secret ssid_password
  fast_connect: True

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: $ap_point
    password: $ap_point_password

captive_portal:


# Enable Home Assistant API
api:
  password: $api_password
  encryption:
    key: $api_encryption_key
  services:
    - service: set_stepper_target
      variables:
        target: int
      then:
        - stepper.set_target:
            id: blind_stepper
            target: !lambda 'return target;'
        - script.execute: record_stepper_position
    - service: set_stepper_speed
      variables:
        speed: int
      then:
        - stepper.set_speed:
            id: blind_stepper
            speed: !lambda 'return speed;'
    - service: set_stepper_position
      variables:
        stepper_position: int
      then:
        - stepper.report_position:
            id: blind_stepper
            position: !lambda "return stepper_position;"
        - stepper.set_target:
            id: blind_stepper
            target: !lambda "return stepper_position;"

globals:
  - id: open_position
    type: int
    initial_value: $initial_open_position
  - id: closed_position
    type: int
    initial_value: $initial_closed_position
  - id: saved_position
    type: int
    initial_value: $initial_saved_position
    restore_value: true

stepper:
  - platform: a4988
    id: blind_stepper
    dir_pin: D4
    step_pin: D3
    sleep_pin: D2
    max_speed: $stepper_max_speed
    acceleration: $acceleration_value
    deceleration: $deceleration_value

cover:
  - platform: template
    id: blind_cover
    device_class: blind
    name: ${name_of_board} Blinds
    has_position: true
    optimistic: false
    open_action:
      - logger.log: "Opening"
      - cover.template.publish:
          id: blind_cover
          current_operation: OPENING
      - stepper.set_target:
          id: blind_stepper
          target: !lambda "return id(open_position);"
      - while:
          condition:
            lambda: 'return id(blind_stepper).current_position < id(open_position);'
          then:
            - script.execute: update_cover_position
            - delay: 1000 ms
      - script.execute: update_cover_position
      - script.execute: record_stepper_position
      - cover.template.publish:
          id: blind_cover
          current_operation: IDLE
    close_action:
      - logger.log: "Closing"
      - cover.template.publish:
          id: blind_cover
          current_operation: CLOSING
      - stepper.set_target:
          id: blind_stepper
          target: !lambda "return id(closed_position);"
      - while:
          condition:
            lambda: 'return id(blind_stepper).current_position > id(closed_position);'
          then:
            - script.execute: update_cover_position
            - delay: 1000 ms
      - script.execute: update_cover_position
      - script.execute: record_stepper_position
      - cover.template.publish:
          id: blind_cover
          current_operation: IDLE
    position_action:
      - logger.log: "Setting position"
      - stepper.set_target:
          id: blind_stepper
          target: !lambda 'return (float(pos) * float( float(id(open_position)) - float(id(closed_position)) )) + float(id(closed_position));'
      - while:
          condition:
            lambda: 'return id(blind_stepper).current_position != ((float(pos) * float( float(id(open_position)) - float(id(closed_position)) )) + float(id(closed_position)));'
          then:
            - script.execute: update_cover_position
            - delay: 1000 ms
      - script.execute: update_cover_position
      - script.execute: record_stepper_position
      - cover.template.publish:
          id: blind_cover
          current_operation: IDLE
    stop_action:
      - logger.log: "Stopping"
      - cover.template.publish:
          id: blind_cover
          current_operation: IDLE
      - stepper.set_target:
          id: blind_stepper
          target: !lambda 'return id(blind_stepper).current_position;'
      - script.execute: update_cover_position
      - script.execute: record_stepper_position

sensor:
  - platform: template
    name: ${name_of_board} Current position
    lambda: return id(blind_stepper).current_position;
    update_interval: 5s
  - platform: wifi_signal
    name: ${name_of_board} Wifi
    update_interval: 60s
binary_sensor:
  - platform: status
    name: ${name_of_board} Status
script:
  - id: update_cover_position
    then:
      - cover.template.publish:
          id: blind_cover
          position: !lambda 'return float( float(id(blind_stepper).current_position) - float(id(closed_position))) / float( float(id(open_position)) - float(id(closed_position)) );'
  - id: record_stepper_position
    then:
      - globals.set:
          id: saved_position
          value: !lambda 'return id(blind_stepper).current_position;'

HTH.

PS: If your blinds are large then you may want to look at more expensive motors like Nema which have better performance characterstics.

1 Like

So I am trying your code to control a damper project I am working on.
My concern is if the stepper positioning gets out of sync with my damper - it could strip gears or snap the panel.

I see that this appears to save the last position to overcome this but in a recent test, it did not work.
I also see that you had another thread trying to get more information on this feature. Flash memory

Have you had any problems with losing positioning? How do you reset/correct if so?

Add 2 limit switches, one fully open and fully closed. If either switch is turned on then stop the stepper and update the position to open/closed based on which limit switch is turned on… You can buy micro switches from Amazon and they work great…

1 Like

Thanks, seems logical enough. I’ve tested for a couple of day and I think I will be okay without them but still interested for future versions. I’ll have to play with the code to understand more.

I know it’s been a while since this thread has moved, but was wondering about using the limit switch or a reed switch. Do you have any example code of what you had to change to make this work?

Look at the documentation. This is called “human readable” code because its pretty self explanatory what it does and how you use it, they also give you lots of examples.