루덴스코드 Blog

ESP8266 와 PMS7003 을 사용한 IOT 미세먼지 측정기



사진에는 개발 중 테스트를 위한 별도의 NodeMCU 가 하나 더 있습니다. 




PMS7003 이 Fritzing 에 없으므로 10 핀 짜리 커넥터와 만능기판을 사용해서 비슷하게 만들어 사용했다.


ESP8266 에서 할 일 


1. nodeMCU 를 사용하여 PMS7003 의 먼지데이터를 가져온다.

2. nodeMCU 를 사용하여 AP 에 연결한다.(스마트폰으로 nodeMCU 를 AP 에 연결한다.)

3. AP 와 연결된 nodeMCU 는 Thingspeak 와 sparkfun 서버에 값을 전송한다.

4. Lua 언어를 사용하지 않고 아두이노 IDE 를 사용하여 작업한다.

5. PUT/GET 을 사용하여 서버에 값을 전달한다.





세부 작업 내용


## Arduino IDE 에서 ESP8266 보드 사용을 위한 준비 


1. Arduino IDE 실행

2. 파일(FILE)

3. 환경설정(Preference)


Additional Board Manager URLs : 에   http://arduino.esp8266.com/stable/package_esp8266com_index.json  입력 


4. Tools 에서 Board : >> Board Manager 실행

5. esp8266 검색 후 설치(Install)

6. Tools >> Board >> NodeMCU 1.0 (ESP-12E Module) 선택

7. Sketch >> Include Library >> Manage Libraries 선택

8. WiFiManager 검색 후 설치






esp8266 검색


NodeMCU 1.0 (ESP-12E Module) 선택



WiFiManager 검색 후 설치 (ESP8266 보드 설치 후 자동으로 설치되는 예제와 라이브러리 외에 필요)




## PMS7003 와 NodeMCU 연결


NodeMCU 는 1개의 하드웨어 시리얼을 가지고 있으며, 이 시리얼이 다른 포트로 연결되어 사용할 수 있다. 이때 Serial.swap() 함수를 사용한다.


단, 사용해 보았으나 펌웨어의 안정성문제인지 지나치게 노이즈가 많아서 도저히 사용할 수 없을 정도로 판단되었다. Serial.swap() 함수는 사용하지 않았다.


소프트웨어 시리얼의 사용은 아두이노에서 사용했던 방법이다. NodeMCU 에서도 이 방식을 사용해보려고 하였으나 제대로 된 값을 받아내지 못했다. 다른 작업을 하면서 소트프웨어시리얼로 통신을 하는 것이 아직까지 안정적이지 못해 보인다.


하드웨어 시리얼 사용하기로 결정.


결국 RX, TX 를 사용하기로 결정했다. 단, USB 로 PC 와 연결해서 프로그램실행파일을 업로드해야 하므로 PMS7003 은 실행파일업로드가 마쳐진 다음에 NodeMCU 와 연결하면 된다. 개발과정중에 이러한 작업을 반복해야 하므로 두개의 슬라이드 스위치를 두었다. 두개의 스위치는 NodeMCU 의 RX/TX 와 PMS7003 의 TX/RX 를 연결하거나 끊는 용도로 사용한다. 즉, NodeMCU 에 프로그램을 업로드할 때는 스위치를 끄고, 업로드후에는 다시 스위치를 켜서 PMS7003 과 NodeMCU 가 서로 통신이 되도록 하였다.




AP-SW 는 D3 (GPIO0) 의 핀에 해당되며 여기에 들어오는 입력값에 따라 AP 선택이 가능해진다.



## NodeMCU Firmware


아두이노의 max(), min() 이 ESP8266 에서 제대로 작동하지 않는 문제를 발견하고 재정의하여 사용하였다. #include <ESP8266WiFi.h> 이후에 다음의 내용을 넣으면 된다.


#undef max

#define max(a,b) ((a)>(b)?(a):(b))

#undef min

#define min(a,b) ((a)>(b)?(b):(a))


전체 프로그램이 많아 져서 ino 파일과 h 파일로 나눠서 저장하였다. 각각의 다음과 같다.



아두이노 소스

/*


  PMS7003 - ESP8266 (nodeMCU)

  WiFi Selectable AP & Clients

     D3 (GPIO0) --- > PULLUP 

CLIENT 1 - Thingspeak

CLIENT 2 - data.sparkfun.com 

  made by winduino.co.kr (2016.10.28.)

  

*/


#include <ESP8266WiFi.h>

#include <ESP8266WebServer.h>

#include <DNSServer.h>

#include <WiFiManager.h>          //https://github.com/tzapu/WiFiManager


#include "ThingSpeak.h"

#include "PMS7003s.h"


    // Thingspeak

    unsigned long myChannelNumber = 111111;

    const char * myWriteAPIKey = "11111111111111";


    // sparkfun client using get

    const char* host = "data.sparkfun.com";

    const char* streamId   = "11111111111112";

    const char* privateKey = "11111111111113";


#define TRIGGER_PIN   0

#define  DEBUG        1

#define  MEAN_NUMBER  10

#define  MAX_PM       0

#define  MIN_PM       32767

#define  VER          20161025


#ifndef    MAX_FRAME_LEN

#define    MAX_FRAME_LEN   64

#endif


#undef max

#define max(a,b) ((a)>(b)?(a):(b))

#undef min

#define min(a,b) ((a)>(b)?(b):(a))


int status = WL_IDLE_STATUS;


//const int MAX_FRAME_LEN = 64;


int pm1_0=0, pm2_5=0, pm10_0=0;

unsigned int tmp_max_pm1_0, tmp_max_pm2_5, tmp_max_pm10_0; 

unsigned int tmp_min_pm1_0, tmp_min_pm2_5, tmp_min_pm10_0; 

byte i=0;


unsigned long previousMillis = 0;

const long interval = 1000;

bool ledState = LOW;

bool startNumber =  true;



WiFiClient  client;


void setup() {

  pinMode(LED_BUILTIN, OUTPUT);     

  pinMode(TRIGGER_PIN, INPUT);

  Serial.begin(9600);

}


void loop() {

  digitalWrite(LED_BUILTIN, LOW);     delay(500);                     

  digitalWrite(LED_BUILTIN, HIGH);    delay(500); 


  if ( digitalRead(TRIGGER_PIN) == LOW ) {

    wifiManagerStart();

    startNumber = ~startNumber;

  }


  else {

    if (startNumber == true ) { 

      ThingSpeak.begin(client);

      startNumber = false;

    }


  if(i==0) { 

    tmp_max_pm10_0 = tmp_max_pm2_5 = tmp_max_pm1_0  = MAX_PM;

    tmp_min_pm10_0 = tmp_min_pm2_5 = tmp_min_pm1_0  = MIN_PM;

  }

  

  if (pms7003_read()) {

    tmp_max_pm1_0  = max(PMS7003S.concPM1_0_CF1, tmp_max_pm1_0);

    tmp_max_pm2_5  = max(PMS7003S.concPM2_5_CF1, tmp_max_pm2_5);

    tmp_max_pm10_0 = max(PMS7003S.concPM10_0_CF1, tmp_max_pm10_0);

    tmp_min_pm1_0  = min(PMS7003S.concPM1_0_CF1, tmp_min_pm1_0);

    tmp_min_pm2_5  = min(PMS7003S.concPM2_5_CF1, tmp_min_pm2_5);

    tmp_min_pm10_0 = min(PMS7003S.concPM10_0_CF1, tmp_min_pm10_0);

    pm1_0 += PMS7003S.concPM1_0_CF1;

    pm2_5 += PMS7003S.concPM2_5_CF1;

    pm10_0 += PMS7003S.concPM10_0_CF1;

    i++;

  }


  if(i==MEAN_NUMBER) {


    pm1_0 = ((pm1_0-tmp_max_pm1_0-tmp_min_pm1_0)/(MEAN_NUMBER-2));

    pm2_5 = ((pm2_5-tmp_max_pm2_5-tmp_min_pm2_5)/(MEAN_NUMBER-2));

    pm10_0= ((pm10_0-tmp_max_pm10_0-tmp_min_pm10_0)/(MEAN_NUMBER-2));


    thingSpeakClient(pm1_0, pm2_5, pm10_0);

    sparkfunClient(pm1_0, pm2_5, pm10_0);

    

    delay(20000); // ThingSpeak will only accept updates every 15 seconds. 

    

    pm1_0=pm2_5=pm10_0=i=0;

    }     

  }

}


void wifiManagerStart() {

    WiFiManager wifiManager;

    if (!wifiManager.startConfigPortal("OnDemandAP")) {

      delay(3000);

      ESP.reset();

      delay(5000);

    }

}


void thingSpeakClient(int pm1_0, int pm2_5, int pm10_0) {

    ThingSpeak.setField(1,pm1_0);

    ThingSpeak.setField(2,pm2_5);

    ThingSpeak.setField(3,pm10_0);

    ThingSpeak.setField(4,VER);

    ThingSpeak.setLatitude(42.0000);

    ThingSpeak.setLongitude(-71.0000);

    ThingSpeak.setElevation(100);

    ThingSpeak.writeFields(myChannelNumber, myWriteAPIKey);  

}


void sparkfunClient(int pm1_0, int pm2_5, int pm10_0){

  const int httpPort = 80;

  if (!client.connect(host, httpPort)) {

    return;

  }

  

  String url = "/input/";

  url += streamId;

  url += "?private_key=";

  url += privateKey;

  url += "&dust10=";

  url += pm1_0;

  url += "&dust100=";

  url += pm10_0;

  url += "&dust25=";

  url += pm2_5;


  client.print(String("GET ") + url + " HTTP/1.1\r\n" +

               "Host: " + host + "\r\n" + 

               "Connection: close\r\n\r\n");

  unsigned long timeout = millis();

  while (client.available() == 0) {

    if (millis() - timeout > 5000) {

      client.stop();

      return;

    }

  }

}

본문 내용 중 myChannelNumber, myWriteAPIKey, streamId, privateKey 는 각각 Thingspeak 와 sparkfun 에서 부여된 번호를 넣으면 된다. (SPARKFUN 은 별도의 회원가입절차 없이 사용할 수 있다)


헤더파일 PMS7003s.h
#ifndef    MAX_FRAME_LEN
#define    MAX_FRAME_LEN   64
#endif

struct PMS7003_framestruct {
    byte  frameHeader[2];
    unsigned int  frameLen = MAX_FRAME_LEN;
    unsigned int  concPM1_0_CF1;
    unsigned int  concPM2_5_CF1;
    unsigned int  concPM10_0_CF1;
    unsigned int  checksum;
} PMS7003S;

bool pms7003_read() {
    bool packetReceived = false;
    unsigned int calcChecksum = 0;
    bool inFrame = false;
    int detectOff = 0;
    char frameBuf[MAX_FRAME_LEN];
    int frameLen = MAX_FRAME_LEN;

    while (!packetReceived) {
        if (Serial.available() > 32) {
            int drain = Serial.available();
            for (int i = drain; i > 0; i--) {
                Serial.read();
            }
        }
        if (Serial.available() > 0) {
            int incomingByte = Serial.read();
            if (!inFrame) {
                if (incomingByte == 0x42 && detectOff == 0) {
                    frameBuf[detectOff] = incomingByte;
                    PMS7003S.frameHeader[0] = incomingByte;
                    calcChecksum = incomingByte; // Checksum init!
                    detectOff++;
                }
                else if (incomingByte == 0x4D && detectOff == 1) {
                    frameBuf[detectOff] = incomingByte;
                    PMS7003S.frameHeader[1] = incomingByte;
                    calcChecksum += incomingByte;
                    inFrame = true;
                    detectOff++;
                }
                else {

                }
            }
            else {
                frameBuf[detectOff] = incomingByte;
                calcChecksum += incomingByte;
                detectOff++;
                unsigned int val = (frameBuf[detectOff-1]&0xff)+(frameBuf[detectOff-2]<<8);
                switch (detectOff) {
                    case 4:
                        PMS7003S.frameLen = val;
                        frameLen = val + detectOff;
                        break;
                    case 6:
                        PMS7003S.concPM1_0_CF1 = val;
                        break;
                    case 8:
                        PMS7003S.concPM2_5_CF1 = val;
                        break;
                    case 10:
                        PMS7003S.concPM10_0_CF1 = val;
                        break;
                    case 32:
                        PMS7003S.checksum = val;
                        calcChecksum -= ((val>>8)+(val&0xFF));
                        break;
                    default:
                        break;
                }

                if (detectOff >= frameLen) {
                    packetReceived = true;
                    detectOff = 0;
                    inFrame = false;
                }
            }
        }
    }
    return (calcChecksum == PMS7003S.checksum);
}


## 실행파일 업로드 방법

위 두개의 파일을 하나의 폴더에 넣는다. .ino 파일(아두이노파일)은 폴더 이름과 동일하게 만들어 둔다. 아두이노 IDE 를 사용해서 .ino 파일을 연다. 파일이 열리고 파일이름 택 옆에 또 하나의 택이 있고 그 이름이 PMS7003s.h 인지 확인한다. 


두개의 슬라이드 스위치를 모두 OFF 시킨 후 (연결이 끊어진 쪽) 컴파일-업로드를 진행한다.



## AP 설정방법

NodeMCU 에는 프로그램이 실행되는 상태이다. 이 상태에서 AP-SW 를 누른 상태에서 NodeMCU 를 리셋(RST버튼) 시킨다. 


AP-SW 누름 -> NodeMCU RST 누름 ->  NodeMCU RST 뗌 -> (약 3초후)AP-SW 뗌


1. 스마트폰으로 OnDemandAP 에 접속


스마트폰이나 기타 WiFi 에 연결이 가능한 기기로 주변 AP 를 찾아보면 OnDemandAP 라는 AP 를 찾을 수 있다. 크롬등의 웹브라우저로 접속한다.

2. 브라우저에서 192.168.4.1 을 쳐서 NodeMCU 에 접속한다. 192.168.4.1 은 고정된 IP 주소이므로 기억해두어야한다.

3. 화면에 나온 버튼을 클릭해서 자신이 사용하는 AP 에 접속한다. (AP는 공유기를 의미)

      


        





공유기에 접속하게 되면 이후 자동적으로 Thingspeak 와 Sparkfun 서버에 접속하여 20초마다 한번씩 3개의 미세먼지데이터를 보내준다. 각각의 사이트로 가서 데이터를 확인한다. AP 정보는 NodeMCU 의 EEPROM 에 저장되므로 AP 가 변경될 때 한번만 설정하면 된다.


Thingspeak 는 데이터를 그래프화해서 쉽게 확인할 수 있게 해 준다.


sparkfun 에서 제공하는 서비스는 별도의 로그인이 필요없다. 데이터를 저장하고 json 이나 csv 등의 형식으로 다운로드 할 수 있다.



NEXT. 라즈베리 서버로 Thingspeak 나 data.sparkfun.com 과 유사한 서비스를 제공해보자. (진행중)







Comment +0

2016.02.13. MOT.Eliot 에서 강의했던 아두이노 오픈강의 첫번째 내용입니다. 음성과 영상을 녹음했으면 더 좋았을텐데, 우선 자료만 올립니다. 다음번에 이 내용으로 동영상강의를 녹화해서 추가하도록 하겠습니다.









Comment +0