DHT22 를 활용한 온도, 습도계 만들기::인터넷으로 온도와 습도, 불쾌지수를 확인하기.
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초에 한 번씩 온도 습도와 불쾌지수를 받아온다. 이를 실시간으로 확인할 수 있으며 불쾌지수에 따라서 위 스샷과 같이 배경 색이 바뀐다. 참고로 위 스샷은 불쾌지수별로 배경색이 바뀌는 것을 테스트하기 위하여 임의의 값을 넣어본 것이다.
아래는 테스트 영상.
만약 데이터 읽기 실패한 경우 에러 화면을 보여주게 된다. (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>
<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><script src='/js' type='text/javascript'></script>
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; }