간단한 IOT 무드등 만들기

예젠에 회사 블로그에 올렸던 글… 

준비물 

   1. NodeMCU v2 

      WIFI 기능이 탑재된 가성비 좋은 마이크로 컨트롤러 보드입니다.        

      

          

   2. USB 와이어 LED  

      오픈마켓 등에서 ‘USB 와이어 LED’ 로 검색하여 구입 가능합니다. 

      

   3. 브레드보드와 점퍼 케이블 

      납땜 없이 전자 회로를 구성하고 테스트 할 수 있습니다.      

   

   4. NPN 트렌지스터 – 2N2222  

     

      

       

  

NodeMCU v2 소개

NodeMCU v2 에는 중국 에스프레시프 시스템사에서 개발된 wifi 기능이 탑재된 MCU, ESP8266-12E 모듈이 탑재되었습니다. 이 기기의 스펙을 간략하게 적어보면 다음과 같습니다. 

   –  802.11 b/g/n 프로토콜 

   –  Wi-Fi Direct (P2P), soft-AP

   –  TCP/IP 프로토콜 

   – 80Mhz 클럭 스피드를 갖는 저전력 32bit CPU 통합. 

   – 3.3v 전원으로 동작. (입력전원  5-12v)

   – 디지털 입출력핀(GPIO)

   – UART, SPI, I2C 프로토콜 지원

   – 64Kbyte SRAM

   – 4MB 플래시 메모리

  아두이노 부트로더를 올리고 C/C++ 를 이용하여 편하게 개발할 수 있으며 가격도 5000원 정도로 참 저렴합니다. ESP8266-12E 는 시리얼 통신 방식 중에 하나인 UART로 프로그램을 올리거나 디버깅 할 수 있지만, UART 와 USB 통신을 변환하는 CP2012 드라이버 칩을 탑재하고 있습니다.  전원부는 AMS1117 를 이용하여 5v에서 12v까지의 전압을 갖는 전원을 3.3v으로 변환하여 공급해줍니다. 하지만, 3.3v를 이용하는 탓에 5v를 사용하는 센서나 기타 개발 보드 등의 기기와 맞물려 사용할 때는 스위치 회로를 구성해야 합니다. 

  아래 그림은 NodeMCU v2 의 핀과 역할에 대한 맵 입니다. 주로 사용하게 될 디지털 입출력(GPIO) 핀은 출력(OUTPUT)모드에서 HIGH 상태와 LOW 상태를 가질 수 있습니다. HIGH 상태에서는 해당 핀에 3.3v의 전압을 갖는 전류가 흐르게 되지만, LOW 상태에서는 전류가 흐르지 않습니다.  GPIO 핀에는 각각의 번호가 있습니다. 아두이노 IDE 를 이용하여 코딩할 때, 이 핀 번호를 지정하여 직접 제어할 수 있습니다. 

      

Step1. 아두이노 IDE 기본 설정

NodeMCU 에서 아두이노를 사용하기 위해서는 다음과 같은 기본 설정 과정을 거쳐야 합니다. 

(1) https://www.arduino.cc/  페이지에 접속후 Software -> Downloads 페이지에 들어가 아두이노 IDE 최신 버전을 다운로드 받습니다. Windows10 운영체제인 경우 스토어에서 아두이노 최신 버전을 받을 수 있습니다. 

(2) 아두이노 IDE 를 실행한 뒤 상단 메뉴의 파일 -> 환경설정 에 들어갑니다. 그 뒤 아래 그림처럼 ‘추가적인 보드 매니저 URLs’ 에 주소 https://arduino.esp8266.com/stable/package_esp8266com_index.json 를 추가하고 확인 버튼을 누릅니다. 

(3) 상단메뉴 툴 -> 보드 -> 보 매니저…(가장 상단) 에 들어갑니다. 아래 그림처럼 보드 매니저 창이 뜨는 것을 확인할 수 있습니다. 검색창에 ‘esp8266’ 를 입력하고 나온 ‘esp8266 by ESP8266 Community’ 패키지를 설치합니다. 

(3) 마지막으로 보드 설정을 해줍니다. 여기서는 NodeMCU v2 를 사용할 것이기 때문에 아래와 같이 설정해 줍니다. 

Step2. 웹서버 만들기


상단 메뉴의 ‘툴 -> 보드’ 에서 NodeMCU 1.0  을 선택하면 ‘파일 -> 예제’ 메뉴에 ESP8266WebServer 라는 항목이 생깁니다.  예제를 통하여 간단한 웹서버를 바로 만들어볼수도 있습니다. 여기서는 IOT 무드등을 컨트롤하기 위한 웹 서버를 만들어보려고 합니다. 

우선 간단한 HTTP API 를 호출할 수 있도록 구현해 보았습니다.

표 : HTTP API 정의






 패스

   파라미터

 반환값

 설명

   /state

   없음

  {“isOn” : true | false, “brightness” : 1-100 }

상태 값(전원, 밝기)을 가져온다.

   /onoff

   isOn=[true | false] 

 전원을 끄거나 켠다.

   /brightness

   value=[1-100] 

 밝기를 조절한다. 

코드: API 서버

#include <ESP8266WiFi.h> #include <WiFiClient.h> #include <ESP8266WebServer.h> #ifndef STASSID #define STASSID "공유기 이름" #define STAPSK "패스워드" #endif const char *_ssid = STASSID; const char *_password = STAPSK; ESP8266WebServer _server(80); // on/off 상태 bool _isOn = false; // 밝기 int _brightness = 100; void setup(void) { // 시리얼 통신 속도(baud rate) 설정. Serial.begin(115200); // WIFI 동작모드. // WIFI_STA, WIFI_AP, WIFI_AP_STA 모드 중에 하나를 사용할 수 있습니다. WiFi.mode(WIFI_STA); // 연결할 WIFI AP의 ssid 값과 패스워드를 설정합니다. WiFi.begin(_ssid, _password); Serial.println(""); // WIFI AP에 연결될 때까지 기다립니다. while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(""); Serial.print("Connected to "); Serial.println(_ssid); Serial.print("IP address: "); Serial.println(WiFi.localIP()); // 상태 확인 API path 및 함수 설정 _server.on("/state", onRequestState); // LED램프 on/off API path 및 함수 설정 _server.on("/onoff", onRequestOnOff); // 밝기조절 API path 및 함수 설정 _server.on("/brightness", onRequestBrightness); // 404 에러 페이지 _server.onNotFound(onNotFound); _server.begin(); Serial.println("HTTP _server started"); } void loop(void) { _server.handleClient(); } // 현재 상태 값을 JSON 형태로 반환합니다. void onRequestState() { String result = String("{"isOn":") + (_isOn ? "true": "false"); result += String(","brightness":") + _brightness + "}"; _server.sendHeader("Access-Control-Allow-Origin", "*"); _server.send(200, "application/json",result); } void onRequestOnOff() { // 파라미터 'isOn'의 값을 가져옵니다. _isOn = String("true") == _server.arg("isOn"); setOnOff(); onRequestState(); } void onRequestBrightness() { // 파라미터 'value' 의 값을 가져옵니다. // 이 값을 1 부터 100까지의 값을 가질 수 있도록 합니다. int value = (String("") + _server.arg("value")).toInt(); if(value < 1) value = 1; else if(value > 100) value = 100; _brightness = value; setBright(); onRequestState(); } void setOnOff() { // 추후 구현을 위하여 비워놓습니다. } void setBright() { // 추후 구현을 위하여 비워놓습니다. } void onNotFound() { String message = "404 File Not Foundnn"; message += "URI: "; message += _server.uri(); message += "nMethod: "; message += (_server.method() == HTTP_GET) ? "GET" : "POST"; message += "nArguments: "; message += _server.args(); message += "n"; for (uint8_t i = 0; i < _server.args(); i++) { message += " " + _server.argName(i) + ": " + _server.arg(i) + "n"; } _server.send(404, "text/plain", message); }

이 코드를 NodeMCU 에 업로드하기 위하여 USB 5 pin 케이블을 아래 사진과 같이 연결하고 아두이노 IDE 상단 메뉴의 ‘툴 -> 포트’ 에서 새로 추가된 포트를 선택합니다. 그 다음 ‘스케치 -> 업로드’ 를 선택하여 기기에 코드를 올릴 수 있습니다. 

위 코드를 자세히 보면 setup() 함수와 loop() 함수가 있는 것을 발견할 수 있습니다. 이 두개의 함수는 아두이노 생명주기를 담당합니다.  setup() 이벤트 함수 내부에서 디지털 입출력 핀 및 라이브러리를 초기화 할 수 있으며, 최초 한 번반 실행됩니다. 그 이후 loop() 이벤트 함수가 무한 연속으로 호출됩니다. 

     

아두이노 디버깅은 주로 시리얼 통신(UART)과 아두이노 IDE 에 포함된 시리얼 모니터를 통하여 할 수 있습니다. 시리얼 모니터 창은 상단 메뉴의 ‘툴 -> 시리얼 모니터’ 를 선택하여 띄울 수 있습니다. 시리얼 통신을 사용하기 위해서는 setup() 함수 내부에서 Serial.begin(int) 를 호출하여 초기화 시켜줘야 합니다. Serial.begin(int) 함수의 인자값으로 시리얼 통신 속도(baud rate)를 줘야하며, 시리얼 모니터 우측 하단에서 동일한 값으로(아래 이미지 빨간 밑줄) 맞춰줘야 내용이 제대로 출력됩니다. 

방금 올렸던 API 서버 코드가 제대로 동작하는지 확인하기 위하여 시리얼 모니터를 띄워보았습니다. 

192.168.10.39 라는 ip 주소를 할당받았음을 알 수 있습니다. 웹 서버를 만들어 기기를 컨트롤 하는 방식은, 조만간 소개할 MQTT 활용법에 비하여 여러가지 불편한점들이 많습니다. 특히 무선 AP 에 접속한 이후 DHCP 를 통하여 동적 ip 주소를 받는 경우 매번 이렇게 시리얼 모니터를 통하여 할당받은 ip 주소를 확인해야 합니다. 가능하다면, 공유기 등의 DHCP 장비 환경 설정에서 접속된 NodeMCU 기기의 맥 어드레스에 특정 ip 를 고정 할당하도록 설정해 놓는 것이 좋습니다. 

아래 코드를 wifi 연결 완료 부분 뒤에 추가하면 시리얼 모니터를 통하여 맥 어드레스도 확인할 수 있습니다.

Serial.print("MAC: ");
Serial.println(WiFi.macAddress());

시리얼 모니터로 알아낸 ip 주소로 각각의 API 를 호출해 보았습니다. 

모두 정상 동작하는 것을 확인할 수 있습니다. 

위 API 를 UI 를 통하여 호출할 수 있도록 간단한 웹 페이지를 만들어 보았습니다. 

NodeMCU v2 의 ESP8266-12E 는 플래시 메모리가 4Mbyte 로 다른 아두이노 기기들에 비해 적은 편은 아니지만, 그래도 복잡하고 화려한 웹 페이지를 넣기에는 한계가 있습니다. 따라서 웹 페이지 코드를 아래처럼 간단하게 구성 하였습니다. 

코드 : index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no">
    <title>Simple Mood Lamp</title>
    <link rel="stylesheet" type="text/css" href="./index.css" />
</head>
<body>
    <div class="out-box">
        <div class="content">

            <div>
           <input type="button" value="" id="onoff-button" onclick="onClickOnOffButton()">
            </div>
            <div class="brightness-content">
                    <div class="float-parent">
                        <div class="float-left">
                                <input type="range" min="1" max="100" value="0" 
                                     id="brightness-slider" oninput="onInputSlider()"
                                      onchange="onChangeBrightness()">
                        </div>
                        <div class="float-left" id="brightness-value"/>
                    </div>
            </div>
        </div>
    </div>
</body>
<script  src="https://code.jquery.com/jquery-3.4.1.min.js" 
         integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="  
         crossorigin="anonymous"></script>
<script src="index.js"></script>
</html>

코드 : index.js

html body {
    padding: 0;
    margin: 0;
    width: 100%;
    height: 100%;

}
.out-box {
    display: table;
    position: fixed;
    width: 100%;
    height: 100%;

}
.content {
    display: table-cell;
    vertical-align: middle;
    text-align: center;
    width: 100%;
}


.brightness-content {
    display: inline-block;
    margin-top: 20px;
}

.float-left {
    float: left
}

.float-parent {
    height: 30px;
    width: 100%;
    text-align: center;
    float: none;
}

#brightness-slider {
    width: 200px;
}

#onoff-button {
    background-color: gray; /* Green */
    border: none;
    color: white;
    width: 150px;
    height: 45px;
    display: inline-block;
    font-size: 20px;
}

코드 : index.css

var _brightnessSliderEle = null;
var _brightnessValueEle = null;
var _buttonEle = null;
var _state = null;


$(document).ready(function() {
    init();
    onState();
});

function init() {
    _brightnessSliderEle = $('#brightness-slider');
    _brightnessValueEle = $('#brightness-value');
    _buttonEle = $('#onoff-button');

}

function onState() {
    $.get("http://192.168.10.39/state").done(function (data)  {
        setState(data);
    }).fail(function (err) {

    });
}

function setState(state) {
    _state = state;
    setBrightness(state.brightness);
    setOnOff(state.isOn);
}

function onClickOnOffButton() {
    _state.isOn = !_state.isOn;
    var param = {isOn : _state.isOn };
    $.post("http://192.168.10.39/onoff",param ).done(function (data)  {
        setState(data);
    }).fail(function (err) {

    });
}

function onChangeBrightness() {
    _state.brightness = _brightnessSliderEle.val();
    var param = {value : _state.brightness};
    $.post("http://192.168.10.39/brightness",param ).done(function (data)  {
        setState(data);
    }).fail(function (err) {

    });
}

function onInputSlider() {
    _brightnessValueEle.text(_brightnessSliderEle.val() + "%");
}

function setBrightness(value) {
    _brightnessValueEle.text(value + "%");
    _brightnessSliderEle.val(value);
}

function setOnOff(isOn) {
    if(isOn === true) {
        _buttonEle.val("ON");
        _buttonEle.css("background-color","#4CAF50")
    } else {
        _buttonEle.val("OFF");
        _buttonEle.css("background-color","#f54b42")
    }
}

위 세개의 웹 소스 코드를 압축하여 API 서버 코드 상단에 다음과 같이 추가합니다. 

#define INDEX_CSS "html body{padding:0;margin:0;width:100%;height:100%}.out-box{display:table;position:fixed;width:.... #define INDEX_JS "var _brightnessSliderEle=null,_brightnessValueEle=null,_buttonEle=null,_state=null;function init(.... #define INDEX_HTML "<!DOCTYPE html><html lang='en'><head><meta charset='UTF-8'><meta name='viewport' content='widt ....

그리고, 아래와 같이 코드를 추가하여 루트 경로로 접속할 때 웹페이지를 출력할 수 있도록 합니다. 

setup() {

// ... 중략 ... 

_server.on("/", onRequestIndexHTML);
_server.on("/index.css", onRequestIndexCSS);
_server.on("/index.js", onRequestIndexJS);

// ...
}


void onRequestIndexHTML() {
  _server.send(200, "text/html ",INDEX_HTML); 
}

void onRequestIndexJS() {
  _server.send(200, "application/javascript ",INDEX_JS); 
}

void onRequestIndexCSS() {
  _server.send(200, "text/css",INDEX_CSS); 
}

아직 LED 조명은 붙이지 않았지만, 웹페이지로 제어할 수 있도록 구현 하였습니다.