아두이노를 이용한 온도/습도/먼지 측정기


일단 EMQ + Kafka + Hadoop + HBase + Spark의 구성으로 클러스터 구성 작업은 마무리가 되었다.
그리고 현재 시점에 추후 정리하겠지만 센서 데이터 처리를 위한 openTSDB와 시각화를 위한 Grafana까지
추가로 설치를 완료한 상태이다.


구슬이 서말이라도 꿰어야 보배라고 아무리 클러스터를 구성하고 각종 시스템을 그럴듯하게 설치를 해놔봐야
전기 먹는 하마 밖에 더 되겠는가? 각 시스템의 용도에 맞는 처리를 해보고 그 흐름을 알아야 진정한 목표를 이루는
것일 터다. 게다가 개발자의 역할을 갖고 있는 나로서는 아마 백날가도 운영을 위한 클러스터에 하둡을 설치할
기회는 없을 것이다. 이렇게 보면 진정한 목표는 이러한 빅데이터 생태계에서 개발자로서의 역할을 활용할 지점을
찾는 것이라 보아야 한다.


어쨌든 기본적으로 필요한 것은 데이터이고 그래서 우선 간단하게 아두이노와 온습도 센서, 먼지 센서 등을 이용하여 
센서데이터를 수집해보기로 했다. 하지만 전혀 간단하지 않았다…ㅠ.ㅠ 오늘은 그 힘든 여정을 살펴보자


흔하디 흔한…하지만 유니크한…


사실 내가 하는 작업의 대다수는 거의 4~ 5년 전에 이미 누군가 시도했던 일들이다. 앞서 진행한 클러스터 구성도
그렇지만 아두이노를 이용하여 온도/습도/먼지를 측정하고 이를 데이터로 저장한 후 챠트를 이용하여 시각화 해서
보여주는 작업은 웬만큼 아두이노를 아는 사람들이라면 이미 오래 전에 해보았던 작업들이다.


그런데…남들 보다 센서 하나 더 붙인 것이 이렇게 특이한 상황을 만들어낼 줄은 꿈에도 몰랐다…ㅠ.ㅠ


일단 구성품을 보자.


아두이노 MEGA 2560

아두이노 메가 2560


ESP8266-01 Wi-Fi 모듈

ESP8266-01


DHT22 온습도 센서

DHT22


GP2Y1010 미세먼지 센서

GP2Y1010


DS3231 RTC 모듈

DS3231


네오픽셀 LED 모듈

네오픽셀 LED


LCD 모듈 (Nokia 5110)

Nokia 5110 LCD


몇가지 부품들은 설명이 좀 필요할 것 같은데…우선 RTC 모듈은 시간의 흐름에 따른 데이터를 저장해야 하기에
시간 정보가 필요하였고 부가적으로 화면에 시간과 날짜를 표시하기 위해 추가하였다. 다음으로 네오픽셀 LED
모듈의 경우 먼지 센서의 측정값에 따라 직관적으로 농도를 표시하기 위해 LED가 필요했는데 일반 LED로 4가지
색상을 표시하거나 RGB LED를 사용하기에는 아두이노 나노에 남는 입출력 핀이 부족하여 조금 비싸지만 네오픽셀
LED를 사용하게 되었다.


주의사항


혹시라도 그럴리는 없겠지만… 이 글을 보면서 바로 작업을 진행하는 분들이 계실지 몰라 주의해야 할 사항을 먼저 
언급해 두어야겠다.


처음 작업을 시작할 때에는 완성품의 부피를 줄이고자 아두이노 나노로 시작을 하였다. 핀수가 적긴 하지만 그래도
위에 언급한 부품들을 꼭 맞게 연결할 수 있을 정도는 되었다. 그러나 다른 곳에서 문제가 생겼다. 많은 센서들을
연결하다보니 사용하는 라이브러리 수가 늘어났고 또 Wi-Fi 모듈을 통해 MQTT 통신을 해야 하다보니 문자열
코딩이 많아졌다.


사실 아두이노로 그래도 적지 않은 작업을 했지만 역시 문돌이는 문돌이라 여전히 모르는 것 천지다보니 처음에는
완성된 온습도계가 자꾸 리셋되는 이유를 몰랐다. 이곳 저곳을 검색한 후 아두이노의 메모리 문제라는 것으로 판단하고 
그 해결책으로 pgmspace를 include하여 문자열에 대해 F함수 처리를 하였지만(F(“string”)) 그것으로는 역부족이었다. 


결국 아두이노를 나노에서 메가로 교체를 하였다(사실 아두이노의 스펙을 잘 몰라 중간에 우노도 한 번 사용을 했다).
이렇게 교체되는 과정에서 아두이노 사이즈가 커지는 바람에 나노나 우노에 맞춰 만든 외장 케이스가 못쓰게 되어버렸다.
열심히 드릴질까지 해가면서 야심차게 만들었는데…ㅠ.ㅠ


참고로 아두이노에는 3가지 영역의 메모리가 있으며 그 중에 개발자들이 활용할 수 있는 영역은 SRAM이다.
아두이노 종류에 따른 메모리 용량은 아래 표와 같다(나노는 우노와 동일하다).



소프트웨어적으로는 MQTT 통신을 위한 PubSubClient 라이브러리에서 주의를 좀 해야 한다.
이 라이브러리를 초기화 하기 위해서는 Wi-Fi 객체가 Client 타입이어야 하며 이를 지원하는 라이브러리가
몇가지 있는데 나는 그 중에 WiFiEsp를 사용하였으며 이러한 라이브러리를 사용하기 위해서는 ESP8266 모듈의
펌웨어도 AT25-SDK112 firmware라는 펌웨어를 사용해야 했다.


결론은…많은 센서를 사용하게 된다면 입출력 핀 문제 뿐만 아니라 메모리 문제 때문에라도 아두이노 메가를 사용하는
것이 안전하다는 점이다.


아두이노와 센서들의 연결


워낙에 많은 센서들을 부착하다보니 전체 배선도를 표시하는 것은 그리기도 또 읽기도 쉽지 않을 것 같아
각각의 센서 연결도를 따로따로 보여주도록 하겠다.


ESP8266-01


DHT22


GP2Y1010


DS3231 RTC


네오픽셀 LED


Nokia 5110 LCD


코드 작성


전체 코드가 조금 길기는 하지만 우선 주석과 함께 모두 올려본다.


//  Wi-Fi 모듈을 PubSubClient와 함께 사용하기 위해 선택한 ESP8266용 라이브러리
#include <WiFiEsp.h>
#include <WiFiEspClient.h>

// MQTT 통신을 위한 라이브러리
#include <PubSubClient.h>

// DHT22 온습도 센서 라이브러리
#include <DHT.h>

// DS3231 RTC 모듈 라이브러리
#include <DS3231.h>

// Nokia5110 LCD 사용을 위한 라이브러리
#include <Adafruit_GFX.h>
#include <Adafruit_PCD8544.h>

// 네오픽셀 LED 사용을 위한 라이브러리
#include <Adafruit_NeoPixel.h>

// 메모리 관리를 위한 F함수 또는 PROGMEM을 사용하기 위한 헤더파일. 아두이노 메가를 사용하는
// 시점에서 큰 의미는 없어졌음
#include <avr/pgmspace.h>

// MQTT 서버 주소
IPAddress server(123, 123, 123, 123);
// 공유기 SSID와 비밀번호
char ssid[] = "ABCDEFG";
char pass[] = "password";

// Wi-Fi 모듈 상태를 표시하기 위한 변수
 int status = WL_IDLE_STATUS;

// WI-Fi 모듈 객체 선언
WiFiEspClient espClient;
// Wi-Fi 객체를 파라미터로 MQTT 통신을 위한 clietn 생성
PubSubClient client(espClient);

// DHT22 사용을 위한 데이터 핀 설정과 모델 타입 매크로 선언
#define DHTPIN    5
#define DHTTYPE   DHT22

// DHT22 사용을 위한 객체 생성
DHT dht(DHTPIN, DHTTYPE);

// GP2Y1010 측정 핀과 측정을 위한 내부 LED 연결 핀 설정
int measurePin = A0;
int ledPower = 6;
 
// 먼지 측정을 위해 필요한 각종 변수 선언 (자세한 내용은 타 사이트 참조)
int samplingTime = 280;
int deltaTime = 40;
int sleepTime = 9680;
 
float voMeasured = 0;
float calcVoltage = 0;
float dustDensity = 0;

float totSum = 0;
float totCnt = 1.0;
float prevD = 0.0;

// DS3231 RTC 모듈 객체 생성
DS3231  rtc(SDA, SCL);
// 날짜와 시간 표시를 위한 변수
char *dateStr;
char *prevDateStr = "";

// Nokia 5110 LCD 사용을 위한 객체 생성
Adafruit_PCD8544 display = Adafruit_PCD8544(7, 8, 9, 10, 11);

// 네오픽셀 LED  사용을 위한 핀 번호 및 LED의 index 매크로 선언
#define PIN 4   
#define LEDNUM 1  
// 네오픽셀 LED 사용을 위한 객체 생성
Adafruit_NeoPixel strip = Adafruit_NeoPixel(LEDNUM, PIN, NEO_GRB + NEO_KHZ800);

void setup() {
  // 시리얼 초기화
  Serial.begin(9600);
  // Wi-Fi 모듈 사용을 위한 Serial1 초기화 
  Serial1.begin(9600);
  WiFi.init(&Serial1);

  // Wi-Fi 모듈 연결 여부 확인
  if (WiFi.status() == WL_NO_SHIELD) {
    while (true);
  }
  
  // AP에 접속
  while ( status != WL_CONNECTED) {
    status = WiFi.begin(ssid, pass);
  }

  // MQTT를 위한 서버 정보 설정 및 콜백함수 설정. 나는 MQTT publishing만 할 것이기에
  // 콜백 함수는 빈 함수로 남겨둠
  client.setServer(server, 1883);
  client.setCallback(callback);
  
  // 기타 설정들은 다름 함수로 뺌
  otherSetting();
  delay(2000);
}

void otherSetting() {
  // DS3231 RTC 모듈 초기화
  rtc.begin();

  // 시작 날짜와 시간을 설정하는 코드로 최초 사용시에만 필요함
//  rtc.setDate(25, 1, 2018);
//  rtc.setTime(23, 21, 45);
//  rtc.setDOW(4);
  
  // 네오픽셀 LED 초기화
  strip.begin();
  pinMode(ledPower,OUTPUT);

  // Nokia 5110 LCD 초기화
  display.begin();
  display.setContrast(65);
  display.setTextSize(1);
  display.setTextColor(BLACK);

  // DHT22 초기화
   dht.begin();
}

void callback(char* topic, byte* payload, unsigned int length) {
  //MQTT 통신에서 subscrib시 필요한 내용이나 나의 경우 publish만 하므로 그냥 비워둠
}

void loop() {
  // 메인 루프에서는 MQTT 통신이 끊어져있으면 연결을 하고 연결되어있으면 데이터를 모아서
  // MQTT 서버로 전송하는 작업을 진행함
  if (!client.connected()) {
    // 연결이 끊어져있으면 다시 연결 시도	
    reconnect();
  } else {
    // MQTT 서버에 연결되어있으면 데이터를 수집하여 MQTT 서버로 보냄
    getValueAndSendData();
    // 위 작업을 5초마다 한 번씩 수행
    delay(5000);
  }
}

void getValueAndSendData() {
  // 온도, 습도측정 값을 담을 변수를 선언하고 DHT22 센서로부터 측정값을 받아옴
  float temperature = dht.readTemperature(); 
  float humidity = dht.readHumidity();
  // 먼지 센서의 경우 조금 복잡한 계산이 필요하여 따로 함수 처리를 함
  float d = getdust();

    // 간혹 먼지 센서에서 노이즈값이 나오므로 이에 대한 처리를 해줌(값자기 100 가까운 값이
    // 나오기도 하나 음수에 대해서만 처리했음
    if (d < 0.0) {
      d = prevD;
    }

    prevD = d;

    // DS3231로부터 요일값을 받아옴
    dateStr = rtc.getDOWStr(FORMAT_SHORT);

    // 현재 먼지 센서의 측정 값을 순간값이 아닌 그 시점까지의 그날 평균으로 보여주고 있으며(1일 평균)
    // 날짜가 바뀌면 다시 처음부터 계산하도록 처리함
    if (prevDateStr == dateStr) {
      totSum += d;
      totCnt += 1.0;
    } else {
      totSum = d;
      totCnt = 1.0;
    }

    prevDateStr = dateStr;

    float avgD = totSum / totCnt;

    // 먼지 측정 값에 따라 네오픽셀 LED의 색상을 변경함
    if (avgD <= 30) {
      strip.setPixelColor(0, 0, 0, 255); 
    } else if (avgD <= 80) {
      strip.setPixelColor(0, 0, 255, 0); 
    } else if (avgD <= 150) {
      strip.setPixelColor(0, 255, 255, 0); 
    } else if (avgD >= 151) {
      strip.setPixelColor(0, 255, 0, 0); 
    }
    strip.show();

    // 센서 데이터들을 JSON 포맷으로 만들어 MQTT 서버로 전송함
    // 이 JSON 포맷은 시계열 데이터용 NoSQL인 openTSDB에 넣기 위한 포맷임
    // 주의할 것은 PubSubClient 라이브러리는 전송 가능한 패킷 사이즈가 128byte로 
    // 기본 설정되어 더 큰 크기의 패킷 전송을 위해서는 MQTT_MAX_PACKET_SIZE 값을
    // 변경해주어야 한다. 나는 넉넉하게 1024로 설정했다.
    char attributes[393];
    String dtStr = String(rtc.getUnixTime(rtc.getTime()));
    String payload = F("[");
          payload += F("{");
          payload += F("\"type\":\"Metric\"");
          payload += F(",");
          payload += F("\"metric\":\"mqtt.home.pcroom\"");
          payload += F(",");
          payload += F("\"timestamp\":"); payload += dtStr; 
          payload += F(",");
          payload += F("\"value\":"); payload += String(temperature); 
          payload += F(",");
          payload += F("\"tags\": {"); 
          payload += F("\"type\":\"temperature\",");
          payload += F("\"loc\":\"pcroom\"");
          payload += F("}},");
          
//    payload.toCharArray( attributes, 131 );
//    client.publish("/mqtt", attributes);
    //Serial.println(attributes);
    
          payload += F("{");
          payload += F("\"type\":\"Metric\"");
          payload += F(",");
          payload += F("\"metric\":\"mqtt.home.pcroom\"");
          payload += F(",");
          payload += F("\"timestamp\":"); payload += dtStr; 
          payload += F(",");
          payload += F("\"value\":"); payload += String(humidity); 
          payload += F(",");
          payload += F("\"tags\": {"); 
          payload += F("\"type\":\"humidity\",");
          payload += F("\"loc\":\"pcroom\"");
          payload += F("}},");

//    payload.toCharArray( attributes, 131 );
//    client.publish("/mqtt", attributes);
    //Serial.println(attributes);
    
          payload += F("{");
          payload += F("\"type\":\"Metric\"");
          payload += F(",");
          payload += F("\"metric\":\"mqtt.home.pcroom\"");
          payload += F(",");
          payload += F("\"timestamp\":"); payload += dtStr; 
          payload += F(",");
          payload += F("\"value\":"); payload += String(avgD); 
          payload += F(",");
          payload += F("\"tags\": {"); 
          payload += F("\"type\":\"dust\",");
          payload += F("\"loc\":\"pcroom\"");
          payload += F("}}");
          payload += F("]");
    
    // String 타입으로 만든 JSON 문자열을 byte 배열로 변경한 후 서버로 publishing
    // "/mqtt"는 토픽 이름
    payload.toCharArray( attributes, 393 );
    client.publish("/mqtt", attributes);

    //Serial.println(attributes);
    
    // 날짜, 시간, 요일, 온도, 습도, 먼지 등의 값을 LCD에 출력
    display.clearDisplay();   
    display.setCursor(0,0);
    display.print(rtc.getTimeStr(FORMAT_SHORT));
    display.print(F(" ["));
    display.print(dateStr);
    display.print(F("]\n"));
    display.println(rtc.getDateStr(FORMAT_LONG, FORMAT_BIGENDIAN, '-'));
    display.print(F("\nT: "));
    display.println(temperature, 1);
    display.print(F("H: "));
    display.println(humidity, 1);
    display.print(F("D: "));
    display.print(avgD, 1);
    display.display();
}

// MQTT 서버에 연결이 안되었을 시 재연결하는 함수
void reconnect() {
  while (!client.connected()) {
    // 아래 connect 함수를 통해 전달하는 3개의 문자열은 그냥 임의로 정해도 됨
    if (client.connect("ARDUINO", "mazdah", "abcdefg")) {
      Serial.println(F("connected"));
    } else {
      Serial.print(F("failed"));
      delay(5000);
    }
  }
}

// 먼지 측정을 위한 함수
// 고수들은 한번 측정된 값을 사용하지 않고 짧은 시간에 여러번 측정을 하여 그 평균을 사용함
float getdust(){
  digitalWrite(ledPower,LOW);
  delayMicroseconds(samplingTime);
  voMeasured = analogRead(measurePin);
  
  delayMicroseconds(deltaTime);
  digitalWrite(ledPower,HIGH);
  delayMicroseconds(sleepTime);
  
  calcVoltage = voMeasured * (5.0 / 1024);
  dustDensity = (0.17 * calcVoltage - 0.1) * 1000.0;
  return(dustDensity);
}


코드가 좀 길기는 하지만 어려운 코드는 없다. 다만 나의 경우 앞서 말한 것 처럼 메모리 용량이 적은 아두이노를 
사용했다가 원인불명(?)의 리셋으로 한참을 시간 낭비 했고 MQTT 라이브러리의 특성을 잘 몰라 또 한참 시간을 
낭비하게 되었다. MQTT 관련한 시간 낭비 중 하나는 코드에 주석처리한 전송 패킷 사이즈 문제였고 다른 한가지는
지속적으로 데이터를 송신하지 않으면 네트워크가 끊어지는 것이 당연한데 데이터를 보내지도 않으면서 자꾸
MQTT 네트워크가 끊어지는 것이 코드나 센서 이상이라고 생각해서 꽤나 고민했다. 결국은 내가 데이터를 보내기
시작하면서 이해를 하게 되었다.


결과물


처음 아두이노 나노와 우노를 이용해서 만들었을 때 그에 준하여 케이스를 만들었다. 예전에 샀던 블루투스
스피커의 아크릴 포장 케이스가 마침 딱 알맞은 크기였기에 거기에 공기를 통하게 하기 위한 구멍을 열심히
뚫어서 나름 예쁜 모양을 만들었는데…


메모리 문제로 아두이노 메가로 바꾼 후 더이상 아두이노와 센서들이 깔끔하게 케이스 안으로 들어가질 않는다.
결국 내장을 뽑아낸 채 이렇게 볼쌍 사나운 모습으로 열심히 데이터를 모으고 있다…ㅠ.ㅠ


사실 모듈 배치를 고려하지 않은 부분도 있었다. 아무래도 연결된 센서(특히 ESP8266)나 아두이노 자체에서도
발열이 있기에 아무리 구멍을 뚫었다 할지라도 좁은 공간에 함께 넣게 되면 온도 측정에 심각한 영향을 미치게
된다. 먼지 센서도 가급적이면 통풍이 잘되도록 신경을 써주어야 한다.


정리


시작할 때 생각했던 것보다 많이 어려웠고 또 시간도 많이 잡아먹었다.
하지만 그만큼 새로운 사실들도 많이 알게되어 결코 낭비만은 아니었다고 생각한다.


사실 애초에 온/습도 또는 먼지를 측정하여 처리하는 내용은 워낙 흔해서 수집할만한 다른 데이터가 없을지
많이 고민을 해보았다. 그런데 은근히 만만치가 않았다. 생각 외로 아두이노 센서 중에 데이터를 수집할만한
센서가 그리 많지 않았다. 어떤 데이터를 모을까 생각하며 시간을 축내기보다는 흔한 작업이지만 일단 데이터를
모아보자는 생각에 이와 같은 작업을 진행하게 되었다. 사실 중요한 것은 앞서 구성한 클러스터를 통해
정상적으로 데이터를 처리할 수 있는지 확인하는 것이 우선이었으니까…


다음 포스팅에서 정리하겠지만 우선 이렇게 만든 온습도계를 통해 데이터를 모아 MQTT 서버를 거치고 Apache
Kafka를 통해 openTSDB에 저장을 하고 Grafana를 통해 시각화 하는 부분까지는 정상적으로 된 것 같다
(하지만 세부적으로는 Kafka쪽에 더 살펴봐야 할 것이 남아있다). 맛보기로 현재 들어오고 있는 데이터에 대한
Grafana 챠트를 올린다.

grafana


일단 기본적인 작업을 성공하였기에 다음에는 조금 더 큰 작업을 해보려고 한다. 내 수준에서 가능할지는 모르겠지만
메카넘 휠을 사용한 차량형 로봇으로 모터의 회전 수와 회전 방향, 거리 센서를 통한 데이터, 충돌 센서 및 충돌 스위치 
센서를 통해 충돌시 실패 감지 등의 데이터를 수집하고 이를 인공지능으로 분석하여 낮은 수준의 자율주행차를 만들어
볼 계획이다. 내 지식 수준에서 바로 될 수는 없는 장기 목표라서 올해 말쯤에나 결과를 볼 수 있겠지만…^^;;;


이 작업이 성공하면 다음은 자율주행 드론에 도전!!!


암튼 하고 싶은 것이 널려서 참 행복한 한 해가 될 것 같다.
그래도 우선 다음 포스팅에서는 센서데이터 -> MQTT -> Kafka -> openTSDB -> Grafana로 이어지는 데이터
처리에 대해 알아보도록 하겠다.

블로그 이미지

마즈다

이미 마흔을 넘어섰지만 아직도 꿈을 좇고 있습니다. 그래서 그 꿈에 다가가기 위한 단편들을 하나 둘 씩 모아가고 있지요. 이 곳에 그 단편들이 모일 겁니다...^^

티스토리 툴바