본문 바로가기

디바이스/아두이노

아두이노 창문 블라인드 자동화 하기


저렴하고 크기도 작은 아두이노 ESP8266, WeMos D1 Mini 보드를 사용하여, 창문 브라인드를 열고 닫는 일을 자동화해 봤습니다. 아침, 저녁 정해놓은 시간에 자동으로 열고 닫을 수 있고, 조이스틱으로 수동으로 올리고 내리는 것도 가능하도록 만들어 봤어요.



게으른 자를 위한, 안 비싼 홈 오토메이션

아침에 일어나면 늘 해야하는 일중 하나는 커튼 이나 블라인드를 걷어 하루종일 햇볕이 은은하게 들어오게 하는 일이다. 그래야 창가에 화분들이 파릇파릇하게 되고, 집안 분위기도 밝아지는 느낌이라 좋다.

그런데, 실상은 아침에 몽롱한 정신으로 겨우 자리에서 일어나서 급하게 출근 준비를 하다보면, 잊어버리는 경우가 더 많다. (아침형 인간은 너무 어려워 ㅠㅠ)

매일의 일상이지만 잊어버리기 쉬운 일로부터 조금씩 더 자유로워지는데는 자동화가 좋은 방법이지 싶다. 시중에 이미 나와 있는 값비싼 홈 자동화 키트를 구매해서 설치하거나, 시공업체 불러서 원래 있던 거 다 걷어내고 새로 설치하면 그만이다. (그런데, 이게 다 돈이라 ㅠㅠ)

또한, 온라인으로 구할 수 있는 약간 저렴한 키트는 제조업체의 앱을 깔아야 사용가능 하도록 만들어져 있더라. 집안의 온갖 것들을 자동화하려고, 제각기 다른 제조업체의 앱을 스마트 폰에 채우고 싶지는 않고, 게다가 그 앱들이 보안상 안전한지도 잘 모르겠다. 

그래서 만들어 봤다 : 아두이노 블라인드 컨트롤러

DIY Arduino Blind Controller


준비물

  • 아두이노 ESP8266 보드 (WeMos D1 Mini가 작아서 적당하다)
  • 스텝 모터와 구동 회로 보드 (소형이라면, 28BYJ-48과 ULN2003, 대형인 경우 Nema 17A4988
  • 아두이노에서 사용할 수 있는  조이스틱 모듈
  • 기타 만능기판, 납땜 인두, 전선 조금, 그리고, 아두이노 개발 환경

(링크는 스펙 참조용, 더 저렴한 곳을 찾아서 사시라)

회로 구성

우선, 회로가 어떻게 구성되는지 보고 간단히 이해하고 넘어가자. (대형 블라인드를 위해 Nema 17 과 A4988보드를 사용하는 경우 바이폴라 방식이라 배선이 약간 다르지만, 소형의 경우만 예로 든다)

  • 스텝 모터를 컨트롤 보드(ULN2003)의 소켓에 연결한다
  • 아두이노(ESP8266)의 D5, D6, D7, D8 핀에 컨트롤러 보드(ULN2003)에 INT1, INT2, INT3, INT4에 각각 위 그림과 같이 연결한다
  • 조이스틱 자체는 위/아래 좌/우 모두 컨트롤 가능하지만, 블라인드가 위/아래 방향만 움직이므로 해당 방향에 대한 선만 연결한다 (Horizontal, Vertical 중 택일하여, 아두이노 보드의 A0에 연결)
  • 조이스틱의 셀렉터(SEL)과 아두이노 보드의 D4를 연결한다 (나중에 조이스틱의 버튼을 누르는 신호를 감지할 수 있다)   
  • 모든 보드에 전원 공급을 위한 5V (+: 빨강) (-:검정) 배선들도 마무리한다

아두이노 코드

먼저 아두이노 개발 환경에서, 이 프로젝트에 필요한 라이브러리를 찾아 설치한다

  • ESP8266
  • Stepper
  • NTPClient

다음과 같은 아두이노 코드를 사용하는데 (전체 코드는 github link 참조), 내가 사용할 와이파이에 대한 정보는 알맞게 코드에서 바꿔준다.

#define WIFI_SSID     "your WiFi SSID"

#define WIFI_PWD      "your WiFi password"

#include <Stepper.h>
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <NTPClient.h>
#include <WiFiUdp.h>


// your WiFi settings
#define WIFI_SSID     "your WiFi SSID"
#define WIFI_PWD      "your WiFi password"

#define BAUD_RATE             57600
#define DELAY_IN_LOOP         200
#define DELAY_IN_WIFI         500

// Joystick Configuration
#define MAX_JOYSTICK          1024 
#define PIN_JOYSTICK_X        A0
#define PIN_JOYSTICK_SW       2   // D4 = GPIO_2
#define SW_OFF                1
#define SW_ON                 0
#define JOYSTICK_THRESOLD     150
#define JOYSTICK_CENTER       (MAX_JOYSTICK / 2)
#define JOYSTICK_RANGE_MIN    200
#define JOYSTICK_RANGE_MAX    900

#define MOTOR_ON_MIN          (JOYSTICK_CENTER - 400)
#define MOTOR_ON_MAX          (JOYSTICK_CENTER + 400)
#define MOTOR_ONOFF_DELAY     100

#define BLIND_OPEN_TIME_MIN   (8 * 60 + 30)  // open time 08:30
#define BLIND_OPEN_TIME_MAX   (16 * 60 + 30) // close time 16:30
#define BLIND_CLOSE           false
#define BLIND_OPEN            true
#define BLIND_CYCLE           200 // it depends on blind types

#define TIME_UPDATE_INTERVAL  0x0000FFFF
#define TIME_ZONE             -5  // your timezone

// Stepper Motor (28YBJ-48)
#define STEPS_PER_RESOLUTION  64
#define MOTOR_SPEED           220

#define DEBUG_BEGIN(baudRate) Serial.begin(baudRate)
#define DEBUG_PRINT(_name, _value) Serial.print(_name); Serial.println(_value)

unsigned long timeUpdate = TIME_UPDATE_INTERVAL;
bool motor_on = false; 
bool blind_open = BLIND_CLOSE;

WiFiUDP udp;
NTPClient ntp(udp, "pool.ntp.org", (TIME_ZONE * 60 * 60), TIME_UPDATE_INTERVAL * 60 * 1000);
Stepper stepper(STEPS_PER_RESOLUTION, D5, D7, D6, D8);

void setup() {
  DEBUG_BEGIN(BAUD_RATE);
  connectWiFi();
  stepper.setSpeed(MOTOR_SPEED);
  motorOn(false);

  runBlind(BLIND_CLOSE);
  ntp.update();
  DEBUG_PRINT("time=", ntp.getFormattedTime());
}

void loop() {
  if (checkJoystick()) {
    // do nothing
  }
  else if (checkTimeOfDay()) {
    // do nothing
  }
}

bool checkJoystick() {
  int x = analogRead(PIN_JOYSTICK_X);
  int should_motor_on = ( MOTOR_ON_MIN > x || x > MOTOR_ON_MAX );
  bool motorUpdated = (should_motor_on != motor_on);

  if (motorUpdated) {
    motor_on = should_motor_on;
    motorOn(motor_on);
  }
  
  if (motor_on) {
    if (x < JOYSTICK_RANGE_MIN) {
      stepper.step(1);  // CW 1 step
    }
    else if (x > JOYSTICK_RANGE_MAX) {
      stepper.step(-1); // CCW 1 step
    }
  }
  return motorUpdated;
}

void motorOn(bool turnOn) {
  int onoff = turnOn ? HIGH : LOW;
  digitalWrite(D5, onoff);
  digitalWrite(D6, onoff);
  digitalWrite(D7, onoff);
  digitalWrite(D8, onoff);
  delay(MOTOR_ONOFF_DELAY);
}

void runBlind(bool openBlind) {
  DEBUG_PRINT("runBlind, open=", openBlind);
  motorOn(true);
  for (int i=0; i<BLIND_CYCLE; i++) {
    stepper.step(openBlind ? 1 : -1);  // CW : CCW
  }
  motorOn(false);
}

bool checkTimeOfDay() {
  bool blindUpdated = false;  
  if (timeUpdate <= 0) {
    if (WiFi.status() == WL_CONNECTED) {
      ntp.update();
    }
    
    DEBUG_PRINT("time=", ntp.getFormattedTime());
    
    int hours = ntp.getHours();
    int minutes = ntp.getMinutes();
    int dailyTime = hours * 60 + minutes;
    bool shouldBlindOpen = ( BLIND_OPEN_TIME_MIN < dailyTime && dailyTime < BLIND_OPEN_TIME_MAX );
    
    blindUpdated = (shouldBlindOpen != blind_open);
    if (blindUpdated) {
      runBlind(shouldBlindOpen);
      blind_open = shouldBlindOpen;
    }
    timeUpdate = TIME_UPDATE_INTERVAL;
  }
  timeUpdate--;
  
  return blindUpdated;
}

void connectWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  delay(DELAY_IN_LOOP);

  Serial.print("device mac: ");
  Serial.println(WiFi.macAddress());

  WiFi.begin(WIFI_SSID, WIFI_PWD);
  Serial.print("Connecting to ");
  Serial.print(WIFI_SSID); 

  Serial.println(" ...");
  
  Serial.println("Waiting for WiFi");
  int i = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(DELAY_IN_WIFI);
    Serial.print(".");
    if (i >= 9) {
      i = 0;
      Serial.println("");
    }
    else {
      i++;
    }
  }
  Serial.println("connected");

  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());
  Serial.println("network is ready!\n");
}

아두이노 에디터에서 verify 아이콘을 눌러 코드가 컴파일이 제대로 되는지 확인한다.

WeMos D1 Mini를 컴퓨터의 USB에 연결하고, 컴파일된 코드가 WeMos D1 Mini로 전송하여 실행될 수 있도록 한다.

3D 모델

블라인드 체인에 연결할 기어 (pully)는 적당한 크기로 3D 모델을 그려서 (Autodesk Fusion 360), 3D 프린터로 뽑아서 연결하였다.

조립과 설치

기판에 부품들을 적당하게 배열하고 납땜으로 기판에 고정한 다음, 기판 아래부분은 나사로 고정시켜 준다.

모두 완성되었으면, 기판의 남는 부분은 잘라내고 3D 프린터로 출력해 놓은 풀리를 모터에 고정시키면 일단 완성이다. 실제 사용할 창문, 블라인드에 설치하기 전, 블라인드의 위치, 모터의 토크 (torque) 등 다양한 요인들을 잘 체크해서 부적합한 사항은 조정해 나가야 한다.

더 해야할 일

작은 블라인드에는 소형 스텝모터로도 충분한 힘(토크)을 전달할 수 있었지만, 큰 블라인드는 어려워서 더 힘센 스텝 모터 (위에 준비물에 적은)를 사용해야 했다. 남은 건 케이스 인데, 얼마간 실험기간을 거친 후, 별 문제가 없는지 기다려 보기로 했다. (급할 거 없으니 …) 

결론

값싸고 초소형인 아두이노 ESP8266 보드(WeMos D1 Mini)를 사용하여, 집 창문에 블라인드를 원하는 시간에 자동으로 열고 닫을 수 있는 블라인드 컨트롤러를 만들어 봤습니다. 때때로 수동으로 열고 닫고 싶을때도 있으니까 조이스틱도 추가했구요. 다음 번에는 아이폰 HomeKit 에 연결하는 기능을 추가해 보기로 해요. <끝>