WIFI로 제어하는 탁상시계 만들기 #8 3D 프린팅 및 조립

2019. 2. 26. 06:59

Project/Turtle Clock

3D 프린팅 출력 및 조립하여 완성하기

오늘은 3D 프린터를 이용하여 출력한 부품들과 앞서 소개한 부품들을 함께 조립하여 시계를 완성하겠습니다. 이 글을 통해 시계 자체는 모두 완성되고 다음 글에서 무선 인터넷을 이용한 사용자 인터페이스를 구성하면 이번 프로젝트는 모두 마무리 됩니다. 출력 과정은 생략하고 출력물부터 소개할 것이고, 별다른 설명은 필요 없어서 사진 위주의 소개글이 될 것입니다.

3D 프린터로 출력한 부품

출력물은 다섯 개입니다. 케이스 좌, 우 그리고 엔코더용 노브(knob)와 네오픽셀 LED용 디퓨저(diffuser), 마지막으로 시계 뒤 커버(cover)입니다. 제가 가진 3D 프린터의 출력 범위가 125mm 밖에 안돼서 케이스를 둘로 나눌 수 밖에 없었습니다. 세그먼트 LED의 폭이 120mm나 되는 바람에 고민을 너무 많이 했네요!

시계 뒤 커버입니다. NodeMCU 보드, 아날로그 광센서, 그리고 배터리 쉴드를 장착합니다.

케이스 좌, 우 부품입니다. 처음에는 한 덩이로 설계했지만, 출력하다보니 어쩔 수 없이 나누게 되었네요. 그래서 디자인이 많이 지저분해졌습니다.

네오픽셀 RGBW LED를 위한 디퓨저입니다. 반투명 재질로 출력했는데, 생각보다 투명한 느낌은  없네요. 세그먼트 LED 아래쪽에 장착됩니다.

로터리 엔코더를 위한 노브입니다. 돌리기 쉽게 약간 크게 디자인했습니다.

부품 조립 및 완성

디퓨저에 네오픽셀 LED를 장착했습니다. 돌출된 LED를 위해 안쪽으로 홈을 내었고, 볼트 홀이 작아서 M2 사이즈의 볼트를 이용해 고정하였습니다.

이렇게 케이스 아래쪽에 끼워 넣고 따로 고정하진 않습니다.

뒤 쪽에서 본 모습입니다. 사진에서 위쪽이 케이스의 아래 부분입니다.

디퓨저 위쪽으로 세그먼트 LED도 삽입하였습니다. 사진에는 일자형 핀 소켓이 보이지만 엔코더와의 간섭 때문에 'ㄱ'자로 변경하였습니다.

앞 쪽에서 본 모습입니다.

전면에는 3mm 두께의 아크릴을 적용하였습니다. 흑색 투명 아크릴이며 밝기는 약간 손해 보는 대신 좀 더 깔끔해 보이는 듯합니다.

뒤 커버는 자주 탈부착해야 하기 때문에 인서트 너트를 이용하기로 했습니다. M3 사이즈입니다.

두 개의 케이스를 서로 고정하는 부분으로 케이스 아래쪽에 있습니다.

반대쪽 케이스도 끼워 주었습니다.

양쪽 케이스 부품을 아래쪽에서 볼트로 고정하였습니다.

케이스 안쪽으로 너트를 위한 육각 홈을 두었기 때문에 볼트를 돌리기만 해도 조여져 고정됩니다.

케이스 상단에 있는 엔코더를 위한 자리입니다. 엔코더를 고정하는 육각 너트를 위한 홈을 만들어 고정하기 쉽게 했습니다.

엔코더를 장착하였습니다. 위쪽에 너트를 삽입 후 아래쪽에서 돌려 끼웠습니다.

뒷 판을 제외한 모든 부품이 조립된 모습입니다.

케이스 뒤 커버 조립 후 완성

후면 커버에 먼저 NodeMCU 보드를 고정했습니다.

다음으로 아날로그 광 센서를 조립하였습니다.

아날로그 광 센서 아래쪽에 센서를 위한 수광부를 만들어 주었습니다.

마지막으로 배터리 쉴드를 장착하였습니다. USB 충전 포트의 위치 때문에 쉴드를 세워서 장착하였습니다.

모든 부품을 장착하고 케이블까지 연결한 모습입니다.

볼트를 인서트 너트에 고정하여 후면 커버를 케이스에 장착하였습니다.

마지막으로 엔코더에 노브를 장착하여 완성하였습니다.

프로그램 소스 작성하여 올리기

이제 완성된 시계에 프로그램을 작성하여 업로드하겠습니다. 업로드할 때는 USB 케이블 연결을 위해 후면 커버를 열어야 합니다.

이제까지 연재를 통해 소개했던 각 부품들을 위한 코드들을 종합하여 아래와 같이 하나의 프로그램으로 완성하였습니다. 자세한 설명은 이미 각 연재 글에서 했기 때문에 여기서는 완성된 소스만 소개하겠습니다.

전체 소스 펼치기
#include <ESP8266WiFi.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <Adafruit_LEDBackpack.h>
#include <Adafruit_NeoPixel.h>
//
// 엔코더 관련 핀 번호
#define outA D5
#define outB D6
#define sw D7
//
// WiFi 접속 정보
const char *ssid     = "coffee";
const char *password = "coffee11";
//
int previousMin = 0;  // 1분마다 NTP 정보 업데이트
//
// LED Segment 관련 변수들
int segBright = 15;           // 숫자판 밝기
int segSleepBright = 4;       // 슬립모드일 때 밝기
// 1초마다 Colon blink, LED effects 효과 내기 위한 변수들
int previousSec = 0;          // 1초 지났는지 체크위해 이전 초값 저장
bool hours12 = true;          // 12, 24 시간제 선택, true - 12시간제
unsigned long colonOnTime;    // 콜론이 켜져있는 시간 체크
bool colonBlink = true;       // 콜론이 깜박일지 여부
bool colonSleepBlink = true;  // 슬립 모드일때 콜론 깜박임 여부
//
// NEOPIXEL LED 관련
bool ledSlide = true;       // LED 슬라이드 효과 여부
bool ledSlideFirst = true;  // 슬라이드 시작할 때 먼저 배경색 채워준다
byte ledCount;              // 몇 번째 LED인지 체크
int bgRed = 100;
int bgGreen = 30;
int bgBlue = 30;
int fgRed = 0;
int fgGreen = 0;
int fgBlue = 255;
int rgbBrightness = 125;
//
// 슬립 모드 관련 변수
bool sleepMode = true;    // 슬립 모드 사용 여부
int startSleepMode = 50;  // 슬립 모드로 들어가는 밝기 값
bool sleepModeOn = false; // 슬립 모드로 들어간 상태인지 체크
                          // 이미 슬립 모드라면 처리하지 않기 위해...
// 엔코더 관련 변수
int previousA = 1;
int previousB = 1;
int previousPush = 1;
int cwCount = 0;
int ccwCount = 0;
int whiteBrightness = 125;
bool whiteOn = false;
unsigned long debounce = 0;
//
WiFiUDP udp;
NTPClient timeClient(udp, "kr.pool.ntp.org", 32400, 3600000);
Adafruit_7segment ledSegment = Adafruit_7segment();
Adafruit_NeoPixel ledBar = Adafruit_NeoPixel(8, D3, NEO_GRBW + NEO_KHZ800);
//
void setup(){
  //
  pinMode(outA, INPUT_PULLUP);
  pinMode(outB, INPUT_PULLUP);
  pinMode(sw, INPUT_PULLUP);
  //
  Wire.begin(D1, D2);
  ledSegment.begin(0x70);
  ledSegment.setBrightness(segBright);
  //
  ledBar.begin();
  ledBar.show();
  ledBar.setBrightness(rgbBrightness);
  ledBarClear();
  //
  wifiConnect();
  //
  timeClient.begin();
}
//
void loop() {
  if ( ( millis() - debounce ) > 1 ) {
    encoderInput();
  }
  //
  ntpUpdate();
  //
  effectSec();
  //
  if ( sleepMode ) {  // 슬립모드 사용일때만 밝기 체크 들어감
    toSleepMode();
  }
}
//
void wifiConnect() {
  WiFi.begin(ssid, password);
  int count = 0;
  while ( WiFi.status() != WL_CONNECTED ) {
    ledSegment.displaybuffer[0] = 0x01 << count;
    ledSegment.displaybuffer[1] = 0x01 << count;
    ledSegment.displaybuffer[3] = 0x01 << count;
    ledSegment.displaybuffer[4] = 0x01 << count;
    ledSegment.writeDisplay();
    //
    count = count + 1;
    if ( count > 5 ) count = 0;
    delay ( 200 );
  }
}
//
void ntpUpdate() {
  timeClient.update();  // NTP 정보 업데이트
  int currentMin = timeClient.getMinutes();
  if (previousMin != currentMin) {    // 1분이 지났는지 체크
    int currentTime = timeClient.getHours() * 100 + currentMin;
    //
    if (hours12) {  // 12시간제로 표시할 경우
      if (currentTime > 1259) {
        ledSegment.print(currentTime - 1200, DEC);
      } else {
        ledSegment.print(currentTime, DEC);
      }
      //
      if (currentTime > 1159) {
        ledSegment.displaybuffer[2] = 8; // PM
      } else {
        ledSegment.displaybuffer[2] = 4; // AM
      }
    } else {    // 24시간제로 표시
      ledSegment.print(currentTime, DEC);
    }
    ledSegment.writeDisplay();
    previousMin = currentMin;
  }
}
//
void effectSec() {
  int currentSec = timeClient.getSeconds();
  if (previousSec != currentSec) {    // 1초가 지났다면...
    // 1초마다 Colon ON
    // Blink 여부와 상관없이 항상 ON
    // AM, PM을 위해 켜져있는 앞쪽 도트랑 OR 연산을 해준다
    ledSegment.displaybuffer[2] = ledSegment.displaybuffer[2] | 0x02;
    ledSegment.writeDisplay();
    colonOnTime = millis(); // 500밀리초가 지나면 콜론 OFF위해 ON 시간 저장
    //
    // 1초마다 네오픽셀 LED 슬라이드 효과 내기
    if ( ledSlide && !sleepModeOn && !whiteOn ) {
      if ( ledSlideFirst ) {    // 처음 출력할 땐, 8개 LED에 먼저 배경색 채운다
        for ( int i = 0; i < 8; i++ ) {
          ledBar.setPixelColor(i, bgRed, bgGreen, bgBlue);
        }
        ledCount = 7;
        ledSlideFirst = false;
      }
      ledBar.setPixelColor(ledCount, bgRed, bgGreen, bgBlue);
      if ( ++ledCount > 7 ) ledCount = 0;
      ledBar.setPixelColor(ledCount, fgRed, fgGreen, fgBlue);
      ledBar.show();
    }
    previousSec = currentSec;
  }
  //
  // 가운데 콜론 깜박임 처리
  if ( colonBlink ) {
    if ( sleepModeOn && !colonSleepBlink ) {
      // 슬립 모드일때 깜박임 안한다면 아무 일도 안함
    } else if (millis() - colonOnTime > 500) { // 콜론이 켜지고 500밀리 초가 지났다면...
      ledSegment.displaybuffer[2] = ledSegment.displaybuffer[2] & 0xFD;
      ledSegment.writeDisplay();
      colonOnTime = millis();
    }
  }
}
//
void ledBarClear() {
  for ( int i = 0; i < 8; i++ ) {
    ledBar.setPixelColor(i, 0, 0, 0, 0);
  }
  ledBar.show();
}
//
void toSleepMode() {
  int lightVal = analogRead(A0);
  if ( ( lightVal <= startSleepMode ) && !sleepModeOn ) { // 정해진 밝기 이하로 떨어지고
                                                     // 아직 슬립 모드가 아닐 때...
    ledSegment.setBrightness(segSleepBright); // 슬립 모드일때의 세그먼트 밝기
    ledBarClear();  // 슬립 모드에서 네오픽셀은 OFF
    sleepModeOn = true;
  } else if ( ( lightVal > startSleepMode + 100 ) && sleepModeOn ) {
    ledSegment.setBrightness(segBright);
    ledSlideFirst = true;
    sleepModeOn = false;
  }
}
//
void encoderInput() {
  int currentA = digitalRead(outA);
  int currentB = digitalRead(outB);
  //
  if ( ( currentA != previousA ) || ( currentB != previousB ) ) {
    if ( currentA == 0 ) {    // A 입력이 "0"일때
      if ( currentB == 0 ) {  // B 입력이 "0"일때
        // 입력이 0,0 일때의 처리
        if ( ( cwCount == 1 ) ) {
          cwCount = 2;
        } else if ( ( ccwCount == 1 ) ) {
          ccwCount = 2;
        }
      } else { // B 입력이 "1"일때
        //  입력이 0,1 일때의 처리
        if ( ( cwCount == 0 ) && ( ccwCount == 0 ) ) {
          cwCount = 1;
        } else if ( ( ccwCount == 2 ) ) {
          ccwCount = 3;
        }
      }
    } else { // A 입력이 "1"일때
      if ( currentB == 0 ) {  // B 입력이 "0"일때
        // 입력이 1,0 일때의 처리
        if ( ( cwCount == 2 ) ) {
          cwCount = 3;
        } else if ( ( cwCount == 0 ) && ( ccwCount == 0 ) ) {
          ccwCount = 1;
        }
      } else { // B 입력이 "1"일때
        // 입력이 1,1 일때의 처리
        if ( ( cwCount == 3 ) ) {
          whiteBrightness += 5;
          if ( whiteBrightness > 255 ) whiteBrightness = 255;
          whiteOn = true;
          whiteLedOn();
          cwCount = 0;
        } else if ( ( ccwCount == 3 ) ) {
          whiteBrightness -= 10;
          if ( whiteBrightness < 1 ) whiteBrightness = 1;
          whiteOn = true;
          whiteLedOn();
          ccwCount = 0;
        }
      }
    }
    previousA = currentA;
    previousB = currentB;
  }
  // 엔코더의 push 스위치 처리
  int currentPush = digitalRead(sw);
  //
  if ( currentPush != previousPush ) {
    if ( currentPush == 0 ) {
      if ( whiteOn ) {
        whiteOn = false;
        ledBarClear();
        ledSlideFirst = true;
      } else {
        whiteOn = true;
        whiteLedOn();
      }
    }
    previousPush = currentPush;
  }
  debounce = millis();
}
//
void whiteLedOn() {
  ledBar.setBrightness(255);
  for ( int i = 0; i < 8; i++ ) {
    ledBar.setPixelColor(i, 0, 0, 0, whiteBrightness);
  }
  ledBar.show();
}

프로그램을 업로드 하고 실행된 모습입니다.

마지막으로 White LED를 이용한 램프를 켜보았습니다.

완성된 모습과 간단한 조작 영상입니다.영상 속에서는 처음 실행시의 구동 영상과 LED 램프를 켜는 모습이 나옵니다. LED 램프는 상단의 노브를 눌러 on/off 하는 모습과 살짝 돌리는 동작으로 램프를 켜는 모습이 이어서 나옵니다.  여기까지 해서 시계는 우선 완성하였고, 다음 글에서는 마지막으로 WiFi를 이용하여 시계를 제어할 수 있도록 서버 및 웹 페이지를 구성하겠습니다. 이상입니다.




7 Comments


profile image
2019.07.02 21:12 신고 Excellent tutorial. Keep up the good work. Will try this project and update. Many thanks for the wonderful project.
profile image
2019.07.02 23:56 신고 Excellent tutorial. Very well explained. Working like a charm. Looking forward to seeing the next article.
Many thanks.
profile image
2019.11.10 16:01 신고 NodeMCU 보드 가격이 얼마에요?

정품보드 인가요?
profile image
2019.12.17 08:15 신고 NodeMCU 보드는 aliexpress에서 $3에 구입한 카피 제품입니다.
profile image
/
2019.12.01 19:52 비밀댓글입니다
profile image
/
2019.12.17 08:18 비밀댓글입니다
profile image
2019.12.23 11:03 신고 세그먼트는 어디서 구입하셨나요? 세그먼트 제품명이 뭐죠?