프로젝트

나도 메이커! 메이커스 여러분들의 작품/프로젝트를 공유하는 공간입니다.

WiFi쉴드를 이용하여 API를 통해 인터넷에서 날씨정보 읽어오기

2015-01-19 15:39:56

개요

개요

아두이노를 통해 날씨정보를 읽어오는 일은 센서를 사용한다면 정말 쉬운 일이다.
tmp36온도센서를 통해 센서 주변의 온도를 읽어올 수도 있고 온습도 센서를 통해 온도와 습도 둘 다 읽어올 수 도 있다.




하지만 센서의 장점은 현재 센서가 감지하고 있는 국지적인 부분만 측정이 가능하고,
센서의 정확도가 떨어진다면 상대적으로도 그 정보의 가치는  떨어지게 된다.

그래서 가끔씩은 센서가 아닌 날씨 전문 기관이나 해외의 유명 관측소에서 제공하는 날씨정보를 아두이노를 읽어오면 어떨까라는 생각을 하게 된다.
물론 통신을 통해 그런 날씨정보를 읽어오는 일은 쉬운일이 아니다. 또한 관측소에서 날씨정보소스를 공개해 주지 않는다면 일반인이 읽어올 수 있는 방법은 없다.





만약 공개해 주지 않는다면 지금 이 글이 존재하였을까?
인터넷에서 여러 기관들은 OpenAPI를 통해 쉽게 제공하는 정보를 User들이 읽어올 수 있도록 하고 있다.

이쯤에서 API에 대해 설명하면 API(Application Programming Interface)란 프로그램이나 어플리케이션이 정보처리를 위해
운영체제에 호출하는 함수나 서브루틴의 집합을 말한다.


출처 : www.instra.com


다르게 설명하면 어떤 한 프로그램이나 어플리케이션을 작동시키기 위해 운영체체나 프로그래밍 언어가 제공하는 인터페이스를 말하는데
우리는 웹API를 통해 웹으로 정보요청을 보내면 웹에서는 그 요청에 맞는 정보를 우리에게 제공하여 준다.
이런 편리한 웹API가 있기 때문에 유저들은 쉽게 인터넷에서 사이트에 방문하지 않고도 정보요청을 통해 정보를 읽어올 수 있다.

이번 글에서는 날씨정보에 대한 웹API를 제공해주는 대표적인 사이트인 OpenWeatherMap을 통해 날씨정보를 읽어올 것이다. 
복잡한듯 보이지만 하나하나 이해한다면 그 과정에 대해 고개를 끄덕이고 응용도 할 수 있을 것이다.




<시리얼 모니터에 날씨정보를 받아와 출력한 모습(지역은 강남이다.)>

동영상




필요한 사전 지식

WiFi 연결하기




부품 목록

NO 부품명 수량 상세설명
1 아두이노 1 오렌지 보드
2 WiFi 실드 1 WiFi모듈

부품명 오렌지 보드 아두이노 WIFI 실드
파트

OpenWeatherMap

OpenWeatherMap에서는 웹 API를 통해 데이터를 XML형식이나 Json형식으로 제공하여 준다.
Json이나 XML모두 프로그래밍을 하지 않은 사용자라면 생소한 용어일텐데 둘 다 데이터의 포맷(형식)이라고 보면 된다.

데이터를 제공해주지만 그 형식은 두가지(Json와 XML)가 있고 우리는 그 두 가지중에 한가지 포맷을 불러와서 데이터를 추출하면 된다.
솔직히 Json이나 XML 둘 다 아두이노 입문자나 프로그래밍 초급자에게는 상당히 어려운 난이도의 문제이기 때문에 기본적인 개념만 설명하고 지나가겠다.

1. XML

XML은 Extensible Markup Language의 약자로 확장가능한 마크업 언어라고 보면 된다.
마크업은 HTML언어를 보게되면 </step>나 </section>와 같은 <>로 둘려 쌓여있는 문구를 볼 수 있는데 우리는 이런것을 태그라고 많이 부르는데
태그는 아마 홈페이지를 만들때나 게시판에 글을 올릴 때 글을 꾸미거나 이미지를 링크하기 위하여 많이 쓰게된다. 이 태그를 마크업이라고 한다.
HTML에서는 이런 마크업이 특정기능으로 정해져 있지만 XML에서는 사용자가 정의해서 따로 확장가능하게 사용할 수가 있다.

아래 첫번째 사진은 Wiki페이지를 구성하는 HTML소스이고
두번째 사진은 OpenWeatherMap에서 제공하는 날씨정보를 XML로 표현한 것들인데 언뜻보면 두 개의 언어가 차이가 없어보인다.
사실 차이가 없어보이는것도 Markup언어를 사용하였기 때문에 눈에는 <>로 둘려쌓여인 언어만 보이게 된다.

하지만 자세히 보게 되면 두번째 OpenWeatherMap에서 제공하는 XML은 <>안의 언어를 새롭게 정의하여 날씨정보를 표현하고 있다.
예를 들자면 <temperature value= 를 통해 온도값을 표현하고 있고, <wind>와  </wind>사이에는 바람에 대한 정보를 담고 있다.

XML은 HTML의 단점을 보완한 언어로 자리 잡으며 데이터 전송이나 DB, 자료검색 등에서 많이 쓰인다.

<Wiki를 구성하는 페이지 소스>



<OpenWeatherMap에서 제공하는 XML형식의 날씨 정보>


2. Json

Json은 Javascript Object Notation의 약자로 XML과 마찬가지로 인터넷에서 자료를 주고받을 때 쓰이는 포맷의 하나이며 자바스크립트의 구문을 따르는 언어이다.
자바스크립트의 구문을 따르지만 딱히 특정 언어에 종속되어 있지 않기 때문에 여러 언어에서 사용가능한 포맷이다.
기본적인 형식은 아래 사진와 같이 {}괄호로 둘려쌓여 있다.
데이터를 표현하는 형식이 XML과는 완전히 다르지만 보고 이해하는데에는 시간이 그리 오래 걸리지 않는다.
이해하기 어렵다면 쉽게 {}중괄호로 둘려쌓여있고 문자열은 큰따옴표("")로 둘려쌓여있고 콤마(,)를 통해 데이터를 나누고 있다면 Json형식으로 이해하면 편하다.



특징으로는 문자열은 큰따옴표("")로 둘려쌓여있고 객체, 배열, 문자, 숫자, boolean값만을 가질 수 있다.

3. OpenWeatherMap날씨 정보를 웹에서 보기

OpenWeatherMap에서 제공해주는 URL을 들어가게되면 Json형식이나 XML형식으로 웹브라우저로 날씨정보를 볼 수 있게 된다.

XML로 날씨 정보 보기 :  api.openweathermap.org/data/2.5/weather?q="지역명"&mode=xml
                          ex) 한국의 날씨보기 : http://api.openweathermap.org/data/2.5/weather?q=korea&mode=xml
-> 지역명에는 보고싶은 지명의 이름을 적어주면 된다. 한국일 경우 korea, 강남일 경우 gangnam, 영국 런던일 경우 london을 적으면 된다.

Json으로 날씨 정보 보기 : api.openweathermap.org/data/2.5/weather?q="지역명"
                          ex) 한국의 날씨보기 : http://api.openweathermap.org/data/2.5/weather?q=korea
-> 지역명에는 보고싶은 지명의 이름을 적어주면 된다. 한국일 경우 korea, 강남일 경우 gangnam, 영국 런던일 경우 london을 적으면 된다.

4. 이런 날씨정보를 아두이노 스케치를 통해 가공하기(Parsing)

웹에서 볼 수 있는 이런 날씨정보를 Wifi쉴드를 통해 아두이노로 가져와 시리얼모니터로 표현한다면 날씨정보를 인터넷에서 가져온 것이 된다.
하지만 XML형식이나 Json형식으로 된 데이터를 그대로 읽어온다면 사용자는 데이터를 분석하고 이해하는데 시간이 오래 걸리게 된다. 
이렇게 데이터를 읽어와서 사용자에게 필요한 데이터만 추출하는 과정을 파싱(parsing)이라 한다.

<parse의 뜻은 "분석하다"라는 뜻이다>


아래 그림과 같이 XML로 표현된 데이터를 자신에게 필요한 데이터만 보기쉬운 형식으로 표현해 줄 수 있다면 사용자는 쉽게 데이터를 분석하고 이해할 수 있다.
이렇게 데이터를 읽어와서 가공까지 끝나야 데이터를 읽어온 것이 된다.

소프트웨어 coding

#include "SPI.h"
#include "WiFi.h"

char ssid[] = "와이파이 SSID";       //와이파이 SSID
char pass[] = "와이파이 password";   //와이파이 password 
String location = "Gangnam"; //날씨정보를 보고싶은 지역


//인스턴스 변수 초기화
WiFiServer server(80);
WiFiClient client;

const unsigned long requestInterval = 60000;  // 요구 시간 딜레이(1 min)

IPAddress hostIp;
uint8_t ret;
unsigned long lastAttemptTime = 0;            // 마지막으로 서버에서 데이터를 전송받은 시간
String currentLine = "";          // 서버에서 전송된 데이터 String저장
String tempString = "";           // 온도 저장 변수
String humString = "";            // 습도 저장 변수
String timeString = "";           // 시간 정보 변수
String pressureString = "";       // 압력 정보 변수
boolean readingTemp = false;      //온도 데이터가 있는지 여부 판단
boolean readingHum = false;       //습도 데이터가 있는지 여부 판단
boolean readingTime = false;      //시간 데이터가 있는지 여부 판단
boolean readingPressure = false;  //압력 데이터가 있는지 여부 판단
int temp = 0;

void setup() {
  //각 변수에 정해진 공간 할당
  currentLine.reserve(100);
  tempString.reserve(10);
  humString.reserve(10);
  timeString.reserve(20);
  Serial.begin(115200);    

  delay(10);
  //WiFi연결 시도
  Serial.println("Connecting to WiFi....");  
  WiFi.begin(ssid, pass);  //WiFi가 패스워드를 사용한다면 매개변수에 password도 작성

  server.begin();
  Serial.println("Connect success!");
  Serial.println("Waiting for DHCP address");
  //DHCP주소를 기다린다
  while(WiFi.localIP() == INADDR_NONE) {
    Serial.print(".");
    delay(300);
  }

  Serial.println("\n");
  printWifiData();
  connectToServer();

}

void loop()
{
  if (client.connected()) {
    while (client.available()) {
      //전송된 데이터가 있을 경우 데이터를 읽어들인다.
      char inChar = client.read();
      // 읽어온 데이터를 inChar에 저장한다.
      currentLine += inChar; 
      //inChar에 저장된 Char변수는 currentLine이라는 String변수에 쌓이게 된다.
      
//라인피드(줄바꿈)문자열이 전송되면 데이터를 보내지 않는다. if (inChar == '\n') { //Serial.print("clientReadLine = "); //Serial.println(currentLine); currentLine = ""; }
//온도 데이터가 전송되었는지 확인 if ( currentLine.endsWith("<temperature value=")) { //현재 스트링이 "<temperature value="로 끝났다면 온도데이터를 받을 준비를 한다. readingTemp = true; tempString = ""; } //<temperature value=뒤에 오는 문자열을 tempString에 저장한다. if (readingTemp) { if (inChar != 'm') { //전송될 문자가 'm'이 올때까지 온도값으로 인식 tempString += inChar; } else { //전송된 문자가 'm'이라면 온도데이터를 그만 저장하고 온도값 출력 readingTemp = false; Serial.print("- Temperature: "); Serial.print(getInt(tempString)-273); Serial.println((char)176); //degree symbol } } if ( currentLine.endsWith("<humidity value=")) { //현재 스트링이 "<humidity value ="로 끝났다면 습도 데이터를 받을 준비를 한다. readingHum = true; humString = ""; } if (readingHum) { if (inChar != 'u') {//전송될 문자열이 'u'가 아니라면 계속 습도값을 받게 된다. humString += inChar; } else { //다음에 전송된 문자열이 'u'라면 습도값을 그만 받고 값을 출력한다. readingHum = false; Serial.print("- Humidity: "); Serial.print(getInt(humString)); Serial.println((char)37); } } if ( currentLine.endsWith("<lastupdate value=")) { // 현재 스트링이 "<lastupdata value="로 끝났다면 마지막 업데이트 시간 데이터를 받을 준비를 한다. readingTime = true; timeString = ""; } if (readingTime) { if (inChar != '/') { //다음 전송될 문자가 '/'가 아니라면 계속적으로 시간데이터를 받는다 timeString += inChar; } else { readingTime = false; Serial.print("- Last update: "); Serial.println(timeString.substring(2,timeString.length()-1)); } } if ( currentLine.endsWith("<pressure value=")) {
// 현재 스트링이 "<pressure value="로 끝났다면 기압 데이터를 받을 준비를 한다. readingPressure = true; pressureString = ""; } if (readingPressure) { if (inChar != 'u') { //다음 전송될 문자가 'u'가 아니라면 계속 기압데이터를 받는다. pressureString += inChar; } else { //다음 전송된 문자가 'u'라면 기압데이터를 출력한다. readingPressure = false; Serial.print("- Pressure: "); Serial.print(getInt(pressureString)); Serial.println("hPa"); } } if ( currentLine.endsWith("</current>")) { //현재 스트링이 </current>로 끝났다면 연결을 끊고 다시 서버와 연결을 준비한다. delay(10000); //10초뒤에 서버와 연결을 끊고 재연결을 시도한다. client.stop(); connectToServer(); //Serial.println("Disconnected from Server.\n"); } } } else if (millis() - lastAttemptTime > requestInterval) { //연결을 실패했다면 requestInterval(60초)이후에 다시 연결을 시도한다. connectToServer(); } }
//서버와 연결 void connectToServer() { Serial.println("connecting to server..."); String content = ""; if (client.connect(hostIp, 80)) { Serial.println("Connected! Making HTTP request to api.openweathermap.org for "+location+"..."); //Serial.println("GET /data/2.5/weather?q="+location+"&mode=xml"); client.println("GET /data/2.5/weather?q="+location+"&mode=xml"); //위에 지정된 주소와 연결한다. client.print("HOST: api.openweathermap.org\n"); client.println("User-Agent: launchpad-wifi"); client.println("Connection: close"); client.println(); Serial.println("Weather information for "+location); } //마지막으로 연결에 성공한 시간을 기록 lastAttemptTime = millis(); } void printHex(int num, int precision) { char tmp[16]; char format[128]; sprintf(format, "%%.%dX", precision); sprintf(tmp, format, num); Serial.print(tmp); } void printWifiData() { // Wifi쉴드의 IP주소를 출력 Serial.println(); Serial.println("IP Address Information:"); IPAddress ip = WiFi.localIP(); Serial.print("IP Address: "); Serial.println(ip); //MAC address출력 byte mac[6]; WiFi.macAddress(mac); Serial.print("MAC address: "); printHex(mac[5], 2); Serial.print(":"); printHex(mac[4], 2); Serial.print(":"); printHex(mac[3], 2); Serial.print(":"); printHex(mac[2], 2); Serial.print(":"); printHex(mac[1], 2); Serial.print(":"); printHex(mac[0], 2); Serial.println(); //서브넷 마스크 출력 IPAddress subnet = WiFi.subnetMask(); Serial.print("NetMask: "); Serial.println(subnet); //게이트웨이 주소 출력 IPAddress gateway = WiFi.gatewayIP(); Serial.print("Gateway: "); Serial.println(gateway); Serial.print("SSID: "); Serial.println(WiFi.SSID()); ret = WiFi.hostByName("api.openweathermap.org", hostIp); Serial.print("ret: "); Serial.println(ret); Serial.print("Host IP: "); Serial.println(hostIp); Serial.println(""); } int getInt(String input){ //String데이터를 intger형으로 변환하는 함수 int i = 2; while(input[i] != '"'){ i++; } input = input.substring(2,i); char carray[20]; //Serial.println(input); input.toCharArray(carray, sizeof(carray)); //Serial.println(carray); temp = atoi(carray); //Serial.println(temp); return temp; }
소스코드를 크게 나누게 되면 각종 인스턴스 및 변수를 정의하는 부분, WiFi쉴드를 통해 연결하는 부분, 서버에서 데이터를 읽어오는 부분, 읽어온 데이터를 파싱해서 필요한 데이터만 출력하는 부분으로 나눌 수 있다.

1. 인스턴스 및 변수를 정의하는 부분

char ssid[] = "와이파이 SSID";       //와이파이 SSID
char pass[] = "와이파이 password";   //와이파이 password 
String location = "Gangnam"; //날씨정보를 보고싶은 지역


//인스턴스 변수 초기화
WiFiServer server(80);
WiFiClient client;

const unsigned long requestInterval = 60000;  // 요구 시간 딜레이(1 min)

IPAddress hostIp;
uint8_t ret;
unsigned long lastAttemptTime = 0;            // 마지막으로 서버에서 데이터를 전송받은 시간
String currentLine = "";          // 서버에서 전송된 데이터 String저장
String tempString = "";           // 온도 저장 변수
String humString = "";            // 습도 저장 변수
String timeString = "";           // 시간 정보 변수
String pressureString = "";       // 기압 정보 변수
boolean readingTemp = false;      //온도 데이터가 있는지 여부 판단
boolean readingHum = false;       //습도 데이터가 있는지 여부 판단
boolean readingTime = false;      //시간 데이터가 있는지 여부 판단
boolean readingPressure = false;  //기압 데이터가 있는지 여부 판단
int temp = 0;
위 부분은 Wifi통신 및 데이터 출력을 위해 필요한 변수들을 선언하는 부분이다. 통신을 위해서는 WiFi의 SSID와 Password가 필요하며,
80포트를 사용하는 서버와 Client가 필요하다.

데이터를 출력하기 위해서는 서버에서 전송된 Character값을 모아서 문자열을 생성하는 currentLine과 
온도, 습도, 시간, 기압값의 존재여부를 판단하는 boolean변수와 저장하는 String변수,
문자열값을 숫자데이터로 바꾼 값을 저장하는 temp변수가 필요하다.

2. WiFi쉴드를 통해 연결하는 부분

void setup() {
  //각 변수에 정해진 공간 할당
  currentLine.reserve(100);
  tempString.reserve(10);
  humString.reserve(10);
  timeString.reserve(20);
  Serial.begin(115200);    

  delay(10);
  //WiFi연결 시도
  Serial.println("Connecting to WiFi....");  
  WiFi.begin(ssid, pass);  //WiFi가 패스워드를 사용한다면 매개변수에 password도 작성

  server.begin();
  Serial.println("Connect success!");
  Serial.println("Waiting for DHCP address");
  //DHCP주소를 기다린다
  while(WiFi.localIP() == INADDR_NONE) {
    Serial.print(".");
    delay(300);
  }

  Serial.println("\n");
  printWifiData();
  connectToServer();

}
Setup()에서는 WiFi쉴드를 통해 웹에 연결을 시도하게 된다.
WiFi.begin(ssid,pass)를 통해 WiFi에 연결을 시도하고 server.begin()을 통해 서버를 초기화 한다.
연결에 성공했다면 "Connect success!"메시지를 출력하게 되며 Wifi정보를 출력하게 되고 

connectToServer()를 통해 클라이언트는 서버에 연결을 시도하게 된다.
void connectToServer() {
  Serial.println("connecting to server...");
  String content = "";
  if (client.connect(hostIp, 80)) {
    Serial.println("Connected! Making HTTP request to api.openweathermap.org for "+location+"...");
    //Serial.println("GET /data/2.5/weather?q="+location+"&mode=xml");
    client.println("GET /data/2.5/weather?q="+location+"&mode=xml"); 
    //위에 지정된 주소와 연결한다.
    client.print("HOST: api.openweathermap.org\n");
    client.println("User-Agent: launchpad-wifi");
    client.println("Connection: close");

    client.println();
    Serial.println("Weather information for "+location);
  }
  //마지막으로 연결에 성공한 시간을 기록
  lastAttemptTime = millis();
}
connectToServer()에서는 client.connect(hostIp, 80)을 통해 80번 포트를 통해 hostIp - api.openweathermap.org에 연결을 시도하게 된다.
연결에 성공하게 된다면 Client는 "GET /data/2.5/weather?q="+location+"&mode=xml" 명령어를 서버로 전송하게 되며 데이터를 요청하게 된다.
location값은 지정한 값으로 검색이 된다.

3. 서버에서 데이터를 읽어오고 데이터를 파싱하는 부분

 char inChar = client.read();
      // 읽어온 데이터를 inChar에 저장한다.
      currentLine += inChar; 
      //inChar에 저장된 Char변수는 currentLine이라는 String변수에 쌓이게 된다.
      
//라인피드(줄바꿈)문자열이 전송되면 데이터를 보내지 않는다. if (inChar == '\n') { //Serial.print("clientReadLine = "); //Serial.println(currentLine); currentLine = ""; }
Client는 서버에서 전송한 데이터가 존재한다면 Client.read()를 통해 inChar에 한글자씩 저장한다.
inChar에서 저장된 문자는 currentLine에 한 글자씩 쌓이게되어 String(문자열)이 된다.
라인피드(줄바꿈)문자열은 별도의 조건문을 주어 따로 저장하지 않는다.

if ( currentLine.endsWith("<temperature value=")) {
        //현재 스트링이 "<temperature value="로 끝났다면 온도데이터를 받을 준비를 한다.
        readingTemp = true; 
        tempString = "";
      }      

      //<temperature value=뒤에 오는 문자열을 tempString에 저장한다.
      if (readingTemp) {
        if (inChar != 'm') { //전송될 문자가 'm'이 올때까지 온도값으로 인식
          tempString += inChar;
        } 
        else { //전송된 문자가 'm'이라면 온도데이터를 그만 저장하고 온도값 출력
          readingTemp = false;

          Serial.print("-  Temperature: ");
          Serial.print(getInt(tempString)-273);
          Serial.println((char)176);    //degree symbol
        }
      }
위 코드는 온도값을 출력하는 부분이다. 이 부분이 XML로 표현된 데이터를 파싱하는 부분인데 XML로 표현된 데이터에서 온도값만 쏙 빼와서 그 값만 출력하게 된다.
아래 사진을 보게 되면 온도 값은 <temperature value ="273.75" 이 부분인데 
소스에서는 서버에서 전송된 문자열이 "<temperature value ="라면 그 다음에 오는 값(273.75)는 온도값으로 간주하고 그 데이터를 온도값으로 저장하게 된다.
마찬가지로 <temperature value="273.75" min="273.75"에서 273.75가 끝나면 min이 나오면서 온도데이터가 끝나게 되는데
소스에서는 조건문 if(InChar != 'm') 을 통해 min의 m이 오면 온도데이터가 끝난것으로 간주하고 온도값을 시리얼모니터로 출력하게 된다.



위의 온도값 출력방법을 이해했다면 아래의 습도값 출력또한 이해가 쉬울 것이다.
      if ( currentLine.endsWith("<humidity value=")) {
        //현재 스트링이 "<humidity value ="로 끝났다면 습도 데이터를 받을 준비를 한다.
        readingHum = true; 
        humString = "";
      }

      if (readingHum) { 
        if (inChar != 'u') {//전송될 문자열이 'u'가 아니라면 계속 습도값을 받게 된다.
          humString += inChar;
        } 
        else { //다음에 전송된 문자열이 'u'라면 습도값을 그만 받고 값을 출력한다.
          readingHum = false;
          Serial.print("-  Humidity: ");
          Serial.print(getInt(humString));
          Serial.println((char)37);
        }
      }
현재까지 전송된 문자열이 <humidity value=로 끝나면 그 다음에 오는 값(위 사진 기준으로 <humidity value="94"  unit="%"/>)은 습도값으로 간주하게 된다.
그렇게 되면 humString에는 94라는 값이 저장되게 되고 다음 문자열이 unit의 u가 온다면 습도값 전송이 끝난것으로 간주하고 습도값을 시리얼 모니터에 출력하게 된다.

if ( currentLine.endsWith("</current>")) { //현재 스트링이 </current>로 끝났다면 연결을 끊고 다시 서버와 연결을 준비한다.
        delay(10000); //10초뒤에 서버와 연결을 끊고 재연결을 시도한다.
        client.stop(); 
        connectToServer();
        //Serial.println("Disconnected from Server.\n");
      }
마지막으로 전송된 문자열이 </current>라면 전송된 데이터의 끝부분을 알리기 때문에 전송을 끊고 다시 재전송 받을 준비를 하게 된다.
delay는 10초를 주었고 10초뒤에 클라이언트는 서버와 연결이 끊기고 다시 재연결을 하여 데이터를 다시 받게 된다.

수박쨈

오렌지보드, WiFi쉴드