아두이노 센서 데이터 전송 - adu-ino senseo deiteo jeonsong

dinist

하나의 보드에서 DHT11 센서와 MQ-5센서를 모두 사용하여 업로드할 것이다.

그럼 하나 소스코드에 DHT11작업과 MQ-5 작업을 모두 해야한다. 또한 DB에 업로드할때 DATETIME도 같이 올려야하므로 NTP를 통하여 시간데이터를 받아와 Time컬럼의 데이터로 활용하도록 한다.

이전에 DHT11 데이터를 수집하기 위한 테이블을 만들었었다. 하지만 지금 생각해보니 만들당시에는 Value를 저장할 컬럼이 하나밖에 없었다. 값을 저장하는 컬럼을 3가지로 늘렸다. (온도,습도,열 지수)

그러면 DHT11 테이블에는 5개의 컬럼이 존재한다. (Seq, Temperature, Humidity, Heat Index, Time)

테이블의 구조를 변경해야하므로 다음 쿼리를 mariadb 콘솔(root로 로그인)에서 입력한다.

alter table DHT11 change Value Temperature TINYINT UNSIGNED NOT NULL;
alter table DHT11 add Humidity TINYINT UNSIGNED NOT NULL;
alter table DHT11 add HI DECIMAL(4,2) NOT NULL;

온도를 저장하는 컬럼 Temperature는 UNSIGNED TINYINT타입으로 정했다. DHT11센서는 0 ~ 50도 까지만 측정가능하므로 0~255까지 범위가 설정된 unsigend tinyint로 설정했다.

Heat Index는 DHT11 센서 테스트당시 xx.xx 형식으로 출력되는것을 확인하여 DECIMAL(4,2)로 설정했다.

총 4자리에 소수점자리 2자리로 설정하는것이다.

이후 MQ-5센서의 값을 저장하기 위한 테이블을 생성한다. MQ-5센서로 부터 수집되는 값은 Rs/R0 값만 저장할 계획이다.

create table MQ5 (
Seq MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
Value DECIMAL(4,2) NOT NULL,
Time DATETIME NOT NULL
);

순서번호를 의미하는 Seq는 UNSIGNED MEDIUMINT으로 설정했다. 최대 값이 1600만이다.

레코드를 1600만개까지 저장할 것 같지는 않지만 이정도로 설정했다.

Rs/R0 값을 저장하는 Value 컬럼은 아까 DHT11의 Heat Index 컬럼의 자료형과 마찬가지로 똑같이 설정했다.

역시 xx.xx형식으로 출력되었기 때문이다.

이제 웹서버에 저장된 post 요청 삽입 파일을 이에 맞게 수정해야한다.

<?php
        $host = "localhost";    // VM의 주소를 입력한다.
        $dbname = "SensorData"; // 데이터베이스명을 입력한다.
        $sqluser = "USERNAME";      // 데이터베이스에 접근할 계정을 입력한다.
        $sqlpass = "USERPASSWORD"; // 계정의 비밀번호를 입력한다.
        $dbcon = "mysql:host={$host};dbname={$dbname};charset=utf8";
                        // 데이터베이스에 연결한다.
                        // 만약 port가 기본 3306이 아닐경우 port=yourport를 위에 추가한다.
                // 또한 charset을 utf8로 설정한다.
        try{
                $Humi = $_POST['Humidity'];
                $Temp = $_POST['Temperature'];
                $Heat = $_POST['HI'];
                $Value = $_POST['MQ5_Value'];
                $Time = $_POST['Time'];
                // POST방식으로 데이터를 전송할 것이므로 POST로 설정 GET으로 데이터 전송시 GET으로 바꾸면 된다.

                $regchk = preg_match("/^(19|20)\d{2}-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[0-1])\s(0[0-9]|1[0-9]|2[0-3]):(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]):(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])$/",$Time);

                // 날짜형식이 2020-05-20 20:01:13 과 같이 정상적 날짜 형식인지 체크하는 정규식이다.
                // 위 정규식에 일치하지않으면 값을 추가할 수 없다.

                if(empty($Value) || empty($Time) || empty($Humi) || empty($Temp)){
                        echo "파라미터값 중 일부가 빈값 입니다.";
                        // body중에 빈값이 있는지 확인
                }else if($regchk == true){

                                // 빈값체크,날짜 정규식 체크를 모두 통과할때 코드

                $tablename = "DHT11";
                // DHT11 테이블에 삽입할것이다

                $db = new PDO($dbcon, $sqluser, $sqlpass);
                //PDO 라는 객체를 이용하여 db접근

                $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
                // 에러 발생시 에러를 EXCEPTION으로 스로잉한다.

                $sql = "INSERT INTO DHT11 (Temperature,Humidity,HI,Time) VALUE (:Temp,:Humi,:HeatIndex,:Time);
                        INSERT INTO MQ5 (Value,Time) VALUE (:Value,:Time);";
                $st = $db->prepare($sql);
                // 선처리 질의문을 이용하여 쿼리를 진행한다.
                // 선처리 질의문은 SQL Injection 방어에도 좋다.

                $st->bindValue(":Temp",$Temp);
                $st->bindValue(":Humi",$Humi);
                $st->bindValue(":HeatIndex",$Heat);
                $st->bindValue(":Value",$Value);
                $st->bindValue(":Time",$Time);
                //POST를 통해 입력 받은 값을 bind 처리해준다.

                $st->execute();
                // execute를 통해 최종적으로 db에 값을 삽입한다.

                echo "데이터 입력이 완료되었습니다.";
                // 모두 완료되면 메시지를 띄운다.

                }else{
                        echo "시간값이 올바르지 않습니다.";
                        // 날짜 정규식 체크를 통과하지 못할 경우 메시지
                }
        } catch(PDOException $e){
                echo "ERROR! Code is ".$e->getMessage();
                // DB관련 에러 발생시 에러코드를 띄운다.
                                // 자세한 메시지를 출력시 DB명 테이블명 컬럼명이 드러날 위험이 있다.
        }
?>

이제 아두이노 코드를 작성하여 아두이노에 업로드한다.

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <DHT.h>
#include <WiFiClient.h>
#include <time.h>

#define SERVER_IP "SERVERIP"	// db서버의 주소
#define DHTPIN 14 				// DHT11 센서의 DATA를 연결한 DIGITAL 포트
#define DHTTYPE DHT11 			// DHT11을 사용함을 명시
#define STASSID "YOUR SSID"		// WiFi SSID 입력
#define STAPSK  "YOU PSK"		// WiFi 비밀번호 입력

DHT dht(DHTPIN, DHTTYPE);

struct tm *lc;					// 내가 원하는대로 날짜형식 작성을 위해 필요한 구조체

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(STASSID, STAPSK);
  configTime(-(3600*9), 0, "1.kr.pool.ntp.org"); // 9시간 시차, 서머타임 적용 X

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("WiFi Connected! IP address: ");
  Serial.println(WiFi.localIP());
  
  Serial.println(F("Gather Temperature, Humidity, Concentrations Of Gases !"));
  Serial.println(F("Wait 60 Seconds...\n"));
  dht.begin();
}

String pluszero(int value){			// localtime을 통해 생성된 구조체의 값이 10 미만이면
  String out;						// 한자리 숫자로 나온다. 예를들어 9일이면 09가 아닌
  if(value < 10){					// 9로 나와서 한자리 숫자일 경우 앞에 0을 추가하는
    out.concat("0");				// 작업을 하는 함수이다.
    out.concat(String(value));
    return out;
  }else{
    out = String(value);
    return out;
  }
}

String calcdate(struct tm *t){				// 현재 시간을 DATETIME 형식으로 바꾸어
  String out = String((1900+t->tm_year));	// String 타입으로 return 한다.
  out.concat("-");
  out.concat(pluszero((t->tm_mon)+1));
  out.concat("-");
  out.concat(pluszero(t->tm_mday));
  out.concat(" ");
  out.concat(pluszero(t->tm_hour));
  out.concat(":");
  out.concat(pluszero(t->tm_min));
  out.concat(":");
  out.concat(pluszero(t->tm_sec));
  return out;
}

void loop() {
  
  delay(60000);  // 측정 간 1분간의 지연을 설정한다.
  WiFiClient client;
  HTTPClient http;
  
  time_t now = time(nullptr);
  lc = localtime(&now);		// 현재 시간을 localtime을 통한 구조체 설정

  // 온도와 습도값을 읽는데 250ms가 소요된다.
  // 센서가 값을 읽을때 2초의 시간이 소요될것이다.
  float h = dht.readHumidity();
  
  // 섭씨온도값으로 온도를 수집한다. (기본값)
  float t = dht.readTemperature();
  
  //float f = dht.readTemperature(true); // 섭씨온도(oC)가 아닌 화씨온도(oF)사용시
                                         // (isFahrenheit = true) 설정을 해준다.

  // 읽기에 실패하였는지 확인하고 만약 그렇다면 빠르게 탈출한다. (빠른 재시작을 위해)
  if (isnan(h) || isnan(t)) {
    Serial.println(F("Failed to read from DHT sensor!"));
    return;
  }

  // 열 지수의 수집을 섭씨로 한다.
  float hic = dht.computeHeatIndex(t, h, false);
  
  float Sensor_Volt;
  float RS_gas;       // GAS의 RS값 저항 관련 값이라 생각하면 됨
  float ratio;        // RS_GAS/RS_air의 비율 값
  float R0 = 0.37;
  int sensorValue = analogRead(A0);
  Sensor_Volt = (float)sensorValue/1024*5.0;
  RS_gas = (5.0-Sensor_Volt)/Sensor_Volt;
  ratio = RS_gas / R0;

  // 온도 습도 영역
  Serial.print(F("Humidity: ")); Serial.print(h);
  Serial.print(F("%  Temperature: ")); Serial.print(t);Serial.print(F("°C "));
  Serial.print(F("Heat index: "));
  Serial.print(hic);
  Serial.println(F("°C"));
  // 가스 영역
  Serial.print(F("sensor_volt = "));
  Serial.print(Sensor_Volt);
  Serial.print(F(" RS_ratio = "));
  Serial.print(RS_gas);
  Serial.print(F(" Rs/R0 = ")); 
  Serial.print(ratio);    //ratio를 기준으로 삼고 ratio가 1밑으로 떨어지면 감지해야함
  Serial.println("");
  // 시간 영역
  Serial.print(F("기준 시간 : "));
  String date = calcdate(lc);
  Serial.println(date);
  Serial.println("");

  if ((WiFi.status() == WL_CONNECTED)) {

    Serial.print("[HTTP] 서버 연결을 시도합니다...\n");
    
    http.begin(client, "http://" SERVER_IP "/insert_post.php");  // 요청을 보낼 URL 입력
    http.addHeader("Content-Type", "application/x-www-form-urlencoded");
    // POST 요청을 할때 전송 방식을 정한다.
    
    Serial.print("[HTTP] 수집한 값의 POST 요청을 시도합니다...\n");
    String POSTBODY = String("Humidity=");
    POSTBODY.concat(String(h));
    POSTBODY.concat(String("&Temperature="));
    POSTBODY.concat(String(t));
    POSTBODY.concat(String("&MQ5_Value="));
    POSTBODY.concat(String(ratio));
    POSTBODY.concat(String("&Time="));
    POSTBODY.concat(String(date));
    POSTBODY.concat(String("&HI="));
    POSTBODY.concat(String(hic));
    int httpCode = http.POST(POSTBODY); // 위에 작성한 URL로 POST 요청을 보낸다.

    if (httpCode > 0) {
      // HTTP 헤더를 전송하고 그에 대한 응답을 핸들링하는 과정
      Serial.printf("[HTTP] 응답 Code : %d\n", httpCode);

      // HTTP 응답 200, 즉 정상응답이면 서버로부터 수신된 응답을 출력한다.
      if (httpCode == HTTP_CODE_OK) {
        const String& payload = http.getString();
        Serial.print("서버로부터 수신된 응답 : ");
        Serial.println(payload);
        Serial.println("");
      }
    } else {		// 에러발생시 에러내용을 출력한다.
      Serial.printf("[HTTP] POST 요청이 실패했습니다. 오류 : %s\n", http.errorToString(httpCode).c_str());
    }

    http.end();
  }
}

결과는...?

아두이노 센서 데이터 전송 - adu-ino senseo deiteo jeonsong

시리얼 모니터에서 수집한 데이터의 값도 잘 나온다. HTTP 응답 코드가 200 OK 이고, 서버에서 데이터 삽입에 성공했을때 보내는 응답인 "데이터 입력이 완료되었습니다." 응답도 잘 보인다.

이제 MariaDB콘솔에서 직접 쿼리를 진행하여 값이 저장되었는지 확인해보자.

아두이노 센서 데이터 전송 - adu-ino senseo deiteo jeonsong
아두이노 센서 데이터 전송 - adu-ino senseo deiteo jeonsong

DHT11 테이블과 MQ5 테이블을 조회해본 결과 위의 시리얼 모니터에 출력된 값과 시간이 모두 잘 나온다.

여기까지 센서를 통해 수집한 값을 아두이노를 통해 Compute Engine VM의 DB에 저장하는것 까지 모두 마쳤다.