개발 관련/아두이노,IOT

DHT22 를 활용한 온도, 습도계 만들기::인터넷으로 온도와 습도, 불쾌지수를 확인하기.

snoworca 2015. 8. 2. 23:35

1. DHT22 와 라이브러리 구하기     



  내가 자주 이용하는 알리 익스프레스에서 3달러 라는 저렴한 가격에 판매되고 있다. 국내 쇼핑몰에서도 많이 판매되고 있다.


  github 에 공개된 오픈소스 라이브러리를 다운 받아서 압축을 풀고 아두이노 프로젝트 폴더 내의 라이브러리 폴더에 붙여 넣는다.

  DHT22 라이브러리 : https://github.com/nethoncho/Arduino-DHT22


  예제코드 : https://github.com/nethoncho/Arduino-DHT22/blob/master/examples/Serial/Serial.ino

  예제코드를 보면 매우 간단한 사용법을 확인할 수 있다.

  

  예제코드 실행 결과. 2초에 한 번씩 온도와 습도를 불러온다. 



2. DHT22 를 활용한 인터넷 온도/습도계 프로젝트. 




   웹 브라우저상에서 비동기 방식(ajax)으로 2초에 한 번씩 온도 습도와 불쾌지수를 받아온다. 이를 실시간으로 확인할 수 있으며 불쾌지수에 따라서 위 스샷과 같이 배경 색이 바뀐다. 참고로 위 스샷은 불쾌지수별로 배경색이 바뀌는 것을 테스트하기 위하여 임의의 값을 넣어본 것이다.


    

아래는 테스트 영상.



  중간에 더러운(?) 소리는 DHT22 온 습도 센서에 입김을 불어넣는 소리다.  
  입김으로 인하여 올라간 습도와 온도 때문에 불쾌지수가 올라가게 되고 따라서 배경색도 바뀌게 된다.


  만약 데이터 읽기 실패한 경우 에러 화면을 보여주게 된다. (timeout 5초 내에 데이터를 가져오지 못 한경우.)





물론 이 상태에서 데이터를 정상적으로 읽게 된다면 원래의 모습으로 돌아온다.





  우선 온도와 습도, 그리고 불쾌지수를 받아올 데이터 타입 정의가 필요하다.


    {"temp":"31.2°C""hr":"80.1%""di":84.85}


  위 JSON 데이터에서 temp 는 온도, hr 은 습도를 의미하고 실수형 데이터를 갖고 있는 di 는 불쾌지수를 나타낸다.

  두 번째로 html 코드를 작성한다. css 파일과 js 파일을 나누어 작성하였다.


HTML:

<!doctype html>
<html>
<head>
    <title>TMP22</title>
    <meta http-equiv='X-UA-Compatible' content='IE=Edge'/>
    <meta charset='UTF-8'>
    <meta http-equiv='Pragma' content='no-cache'>
    <meta HTTP-EQUIV='Expires' CONTENT='-1'>
    <!--모바일 브라우저 호환-->
    <meta name="viewport" content="width=device-width, user-scalable=no">
    <!--모바일 크롬을 위한 테마 컬라--> 
    <meta name="theme-color" content="#333333">
    <!--iOS 사파리를 위한 상태바 컬러-->
    <meta name="apple-mobile-web-app-status-bar-style" content="#333333">

    <script src='https://code.jquery.com/jquery-2.1.4.min.js' type='text/javascript'></script>

<script src='/js' type='text/javascript'></script>

<link href='http://fonts.googleapis.com/css?family=Righteous' rel='stylesheet' type='text/css'> <link href='/css' rel='stylesheet' type='text/css'> </head> <body> <div class="container"> <div class="outer"> <div class="inner"> <div class="block"> <h1 id='temp'>??.? °C</h1> <h1 id='hr'>??.? %</h1> <h2 id='di'>NaN</h2> </div> </div> </div> </div> </body> </html>


  Jquery 라이브러리와 구글 폰트 사이트에서 가져온 Righteous 폰트를 사용한다.

   

Javascript:

    var diMap = [{
        min : 86,
        backColor: '#F44336',
        blockColor: '#C62828',
    },{
        min : 83,
        backColor: '#FF5722',
        blockColor: '#BF360C',
    },{
        min : 80,
        backColor: '#FF9800',
        blockColor: '#E65100',
    },{
        min : 75,
        backColor: '#FFAB00',
        blockColor: '#FF6F00',
    },{
        min : 70,
        backColor: '#009688',
        blockColor: '#004D40',
    },{
        min : 0,
        backColor: '#03A9F4',
        blockColor: '#01579B',
    }];

    /**
     * 불쾌지수별로 색을 바꾼다.
     * @param di 불쾌지수 값.
     */
    function setColorByDI(di) {
        for(i in diMap) {
            if(diMap[i].min <= di) {
                $('body').css('background-color',diMap[i].backColor);
                $('.block').css('background-color',diMap[i].blockColor);
                return;
            }
        }
    }

    function setColorOnError() {
        $('body').css('background-color','#ffffff');
        $('.block').css('background-color','#000000');
    }

    /**
     * 2초에 한 번씩 서버로부터 온도, 습도, 불쾌지수 데이터를 받아온다.
     */
    function loadData(){
        $.ajax({
            url: '/data',
            dataType: 'json',
            timeout: 5000,
            success: function (data) {
                $('#temp').text(data.temp);
                $('#hr').text(data.hr);
                $('#di').text(data.di);
                setColorByDI(data.di);
            },
            error: function(xhr,status,error) {
                $('#di').text('ERROR');
                setColorOnError();
            }
        });
        setTimeout(loadData,2000);
    }
    loadData();
    setTimeout(loadData,2000);



   Javascript 코드는 편한 Ajax 를 이용하기 위하여 Jquery 라이브리를 활용한다.


CSS:

html, body { font-family: 'Righteous', cursive; }
html, body {margin: 0; width: 100%; height: 100%; overflow: hidden }
h1 { font-size: 5em;margin: 0;text-align: center}
h2 { font-size: 3em;margin: 0;text-align: center}
.container {
    width: 100%;
    height: 100%;
}
div.container .outer {
    display: table;
    width: 100%;
    height: 100%;
}
div.outer .inner {
    display: table-cell;
    vertical-align: middle;
    text-align: center;
}
div.inner .block {
    position: relative;
    display: inline-block;
    color: white;
    padding: 1em;
    background: black;
}


  폰트를 이쁜 폰트로 설정하였다.



  아두이노 코드를 작성하기 전에 먼저 이더넷 모듈과 라이브러리를 준비해야 한다. 

  이 예제에서는 ENC28J60 이더넷 모듈을 이용하였고, 이 것에 대한 활용 방법을 다룬 포스팅은 http://www.dev.re.kr/79 에서 확인할 수 있다.


  그리고 이 예제를 수행하기 위하여 ENC28J60 모듈 라이브러리의 EtherCard.cpp 파일을 수정해야 한다. 

  버퍼에 문자열을 기록하는 emit_p 함수가 지정된 크기의 소수점 자리수 출력을 지원하지 않기 때문이다.  

  기존의 Ethercard 라이브러리를 수정된 라이브러리  : https://github.com/ice3x2/ethercard 로 교체하거나, 


  또는 EtherCard.cpp 파일을 열어서 

  

  #define FLOATEMIT


  이 부분에 되어있는 주석을 지우고,


   void BufferFiller::emit_p(PGM_P fmt, ...)


함수 내부의 case 'T': 에서 break; 까지의 부분을 찾아서 삭제한 아래의 코드로 교체한다.


case 'T':
    defaultPa = 3;
    c = pgm_read_byte(fmt);
    if(c != 0 && c == '.') {
        c = pgm_read_byte(++fmt);
        if(c != 0 && c >= '0' && c <= '9') {
            fmt++;
            defaultPa = c - '0';
        } else {
            defaultPa = 0;
        }
    }
    dtostrf(va_arg(ap, double), 10, defaultPa, (char*)ptr );
break;

  


  이렇게 한다면 이제 DHT22 으로 부터 출력되는 데이터를 소수점 첫 번째 자리수까지 끊어서 출력할 수 있다.


   


  이제 아두이노 코드를 작성해야 하는데, 그 전에 HTML 과 CSS 와 Javascript 를 압축 해야 한다. 

   HTML 과 CSS 는 http://www.willpeavy.com/minifier/ 이 곳에서 사이즈를 줄일 수 있고, 

  Javascript 는 http://jscompress.com/ 이 곳에서 사이즈를 줄일 수 있다.



아두이노 코드:

#include <EtherCard.h>
#include <DHT22.h>
#include <string>
#define DHT22_PIN 2
#define CS_PIN 8
#define READ_DHT_DELAY 2000
#define HOST_NAME "TMP22"

#define BUFFER_SIZE 1280


float calcDiscomfortIndex(float temp, float humi);
void readDHT();

char valueStr[6];

static uint8_t macAddr[6] = { 0x54,0x55,0x58,0x1a,0x3c,0x56}; 

byte Ethernet::buffer[BUFFER_SIZE];
BufferFiller bufFiller;

DHT22 dht22(DHT22_PIN);

float temperature = 0.00f; // 온도 
float humidity = 0.00f; // 습도
float di = 0.00f;  // 불쾌지수
long lastReadDHT = 0; // 마지막으로 DHT22 로부터 값을 읽은 시간 (ms)

// CSS 코드를 버퍼에 기록한다.
word writeCss() {
  bufFiller = ether.tcpOffset();
  bufFiller.emit_p(PSTR("HTTP/1.0 200 OK\r\nContent-Type: text/css\r\nPragma: no-cache\r\n\r\n"));
  bufFiller.emit_p(PSTR("html, body{font-family: 'Righteous', cursive;margin: 0; width: 100%; height: 100%; overflow: hidden}h1{font-size: 5em;margin: 0;text-align: center}h2{font-size: 3em;margin: 0;text-align: center}.container{width: 100%; height: 100%;}div.container .outer{display: table; width: 100%; height: 100%;}div.outer .inner{display: table-cell; vertical-align: middle; text-align: center;}div.inner .block{position: relative; display: inline-block; color: white; padding: 1em; background: black;}"));
  return bufFiller.position();
}

word writeFail() {
  bufFiller = ether.tcpOffset();
  bufFiller.emit_p(PSTR("HTTP/1.0 401 Unauthorized\r\nContent-Type: text/html\r\n\r\n<h1>401 Unauthorized</h1>"));
  return bufFiller.position();
}

// 자바스크립트 코드를 버퍼에 기록한다.
word writeJS() {
  bufFiller = ether.tcpOffset();
  bufFiller.emit_p(PSTR("HTTP/1.0 200 OK\r\nContent-Type: application/javascript\r\nPragma: no-cache\r\n\r\n"));
  bufFiller.emit_p(PSTR("var diMap=[{min:86,backColor:'#F44336',blockColor:'#C62828',},{min:83,backColor:'#FF5722',blockColor:'#BF360C',},{min:80,backColor:'#FF9800',blockColor:'#E65100',},{min:75,backColor:'#FFAB00',blockColor:'#FF6F00',},{min:70,backColor:'#009688',blockColor:'#004D40',},{min:0,backColor:'#03A9F4',blockColor:'#01579B',}];function setColorByDI(di){for(i in diMap){if(diMap[i].min<=di){$$('body').css('background-color',diMap[i].backColor);$$('.block').css('background-color',diMap[i].blockColor);return}}}function setColorOnError(){$$('body').css('background-color','#ffffff');$$('.block').css('background-color','#000000')}function loadData(){$$.ajax({url:'/data',dataType:'json',timeout:5000,success:function(data){$$('#temp').text(data.temp);$$('#hr').text(data.hr);$$('#di').text(data.di);setColorByDI(data.di)},error:function(xhr,status,error){$$('#di').text('ERROR');setColorOnError()}});setTimeout(loadData,2000)}loadData();setTimeout(loadData,2000);"));
  return bufFiller.position();
}

// html 코드를 버퍼에 기록한다.
uint16_t writeHtml() {
  bufFiller = ether.tcpOffset();
  bufFiller.emit_p(PSTR("HTTP/1.0 200 OK\r\nContent-Type: text/html\r\nPragma: no-cache\r\n\r\n"));
  bufFiller.emit_p(PSTR("<!doctype html><html><head><meta name='theme-color' content='#333333'><meta name='apple-mobile-web-app-status-bar-style' content='#333333'><meta name='viewport' content='width=device-width, user-scalable=no'><meta http-equiv='X-UA-Compatible' content='IE=Edge'/> <meta charset='UTF-8'> <meta http-equiv='Pragma' content='no-cache'> <meta HTTP-EQUIV='Expires' CONTENT='-1'> <script src='https://code.jquery.com/jquery-2.1.4.min.js' type='text/javascript'></script> <link href='http://fonts.googleapis.com/css?family=Righteous' rel='stylesheet' type='text/css'> <link href='/css' rel='stylesheet' type='text/css'> <script src='/js' type='text/javascript'></script></head><body><div class='container'> <div class='outer'> <div class='inner'> <div class='block'> <h1 id='temp'>??.? °C</h1> <h1 id='hr'>??.? %</h1> <h2 id='di'>NaN</h2> </div></div></div></div></body></html>"));  
  return bufFiller.position();
}

// 온습도, 불쾌지수 값을 JSON 형태로 버퍼에 기록한다. 
word writeJson() {
  bufFiller = ether.tcpOffset();
  bufFiller.emit_p(PSTR("HTTP/1.0 200 OK\r\nContent-Type: application/json\r\nPragma: no-cache\r\n\r\n"));
  bufFiller.emit_p(PSTR("{\"temp\":\"$T.1°C\",\"hr\":\"$T.1%\",\"di\":$T.1}"),temperature, humidity, di);
  return bufFiller.position();
}


void setup(){
  // 이더넷 초기화 
  ether.begin(sizeof Ethernet::buffer, macAddr,CS_PIN);
  // DHCP 초기화.
  while(!ether.dhcpSetup(HOST_NAME,true));
}
 
void loop(){
  word pos = ether.packetLoop(ether.packetReceive());
  readDHT();
  if(pos) {
      ether.httpServerReplyAck(); 
      char* data = (char *) Ethernet::buffer + pos;
      // Data 요청,  
      if(strncmp("GET /data ", data, 10) == 0) {
         ether.httpServerReply_with_flags(writeJson(),TCP_FLAGS_ACK_V|TCP_FLAGS_FIN_V);
      }
      // CSS 요청
      else if(strncmp("GET /css ", data, 9) == 0) {
         ether.httpServerReply_with_flags(writeCss(),TCP_FLAGS_ACK_V|TCP_FLAGS_FIN_V);
      }
      // Javsscript 요청
      else if(strncmp("GET /js ", data, 8) == 0) {
         ether.httpServerReply_with_flags(writeJS(),TCP_FLAGS_ACK_V|TCP_FLAGS_FIN_V);
      }
      else if(strncmp("GET / ", data, 6) == 0) {
         ether.httpServerReply_with_flags(writeHtml(),TCP_FLAGS_ACK_V|TCP_FLAGS_FIN_V);
      } 
      else {
        ether.httpServerReply_with_flags(writeFail(),TCP_FLAGS_ACK_V|TCP_FLAGS_FIN_V);
      }
  }  

}

// DHT22 로부터 값을 읽어온다. 
// 값은 2초 간격으로 읽어온다.
void readDHT() {
    if(millis() - lastReadDHT > READ_DHT_DELAY) {
        lastReadDHT = millis();
        DHT22_ERROR_t errorCode;
        errorCode = dht22.readData();
        switch(errorCode)
        {
            case DHT_ERROR_NONE:
            case DHT_ERROR_CHECKSUM:
              temperature = dht22.getTemperatureC();
              humidity = dht22.getHumidity();
              di = calcDiscomfortIndex(temperature,humidity);
              break;
        }
    }
}

// 온도와 습도를 입력받아 불쾌지수를 계산하여 반환한다.
// 불쾌지수에 대한 계산법은 http://www.kma.go.kr/HELP/basic/help_01_05.jsp 이 곳에서 확인하였다.
float calcDiscomfortIndex(float temp, float humi) {
    return (1.8f*temp)-(0.55*(1-humi/100.0f)*(1.8f*temp-26))+32;
}