Open Weather Map - 날씨 정보 가져오기
이전 글에서 Feather M0 WiFi 보드로 무선인터넷에 접속하는 예제를 살펴봤습니다. 그 중 클라이언트 접속 예제는 간단히 살펴보고 넘어 갔는데, 이번 글에서 좀 더 자세히 다루도록 하겠습니다.
Arduino로 OpenWeatherMap 날씨 정보 가져 오기
WiFi101 라이브러리의 Client 접속 예제는 www.google.com 에 접속해서 arduino에 대한 검색 결과를 가져오는 소스입니다. 원하는 결과는 가져오지 못하지만 접속 및 요청, 응답은 제대로 이루어지기 때문에 과정을 확인하는데는 문제가 없습니다.
이번에는 좀 더 활용 가능한 예제를 다뤄 보려고 합니다. OpenWeatherMap 이라는 날씨 정보 제공 사이트에서 원하는 도시의 날씨를 가져오는 예제입니다. 날씨 데이터를 가져와서 단순히 시리얼 모니터에 출력할 뿐이지만, Client 접속 기능을 알아보는데는 충분할 듯 하네요!
위 사이트를 처음 들어 보시는 분은 위 링크를 눌러 한번 살펴 보세요! OpenWeatherMap.org 에서는 세계 각 도시의 날씨 정보를 제공합니다. 홈페이지에서 원하는 도시명을 검색하여 날씨 정보를 보실 수 있고, 무엇보다 여러가지 날씨 정보를 제공하는 API를 지원합니다.
API는 Application Programming Interface의 약자인데, 쉽게 말하면 홈페이지에서 제공하는 방식외에 스마트폰 어플이나 다른 어플리케이션에서 가공하여 사용하기 쉽도록 데이터를 제공하는 기능입니다. 저는 아두이노 보드에서 접속하기 때문에, 데스크탑에서 웹브라우저로 접근하는 것과는 다른 방식이 필요하고, 이런 API 형태의 자료 제공이 훨씬 다루기 쉽습니다.
요즘, 한국환경공단(AirKorea)의 미세먼지 정보같이 공공데이터 개방, 개방형 데이터 등등 해서 openAPI 방식으로 정보를 제공하는 곳이 많이 있습니다. 기본적인 방식은 동일하므로 이 예제를 통해서 손쉽게 응용하실 수 있습니다.
OpenWeatherMap.org 에서 API를 통해 데이터를 가져오는 건 기본적으로 무료입니다. 데이터 요청(트래픽)이 일정량 이상 넘어가거나 서비스 지원을 원할 때는 유료로 사용해야 하지만 그 전까진 무료로 얼마든지 사용할 수 있습니다. 단, 회원 가입이 요구되며, API 요청시 각 계정마다 고유한 애플리케이션 아이디를 키값으로 넣어줘야 합니다. 물론, 그 과정이 어렵지 않으니 부담 없이 사용하시면 됩니다. 회원 가입과 키값에 대해선 나중에 얘기하고 우선, 기본적인 접속 예제를 구성하도록 하겠습니다.
이전 글에서 다룬 Cilent 예제과 거의 동일합니다. 불러와서 고쳐서 써도 되고, 필요한 부분만 복사해서 써도 되겠습니다. 이 예제는 아래와 같은 순서로 동작합니다.
- 무선 네트워크 접속
- 서버 접속
- 전송받은 데이터 출력
#include <SPI.h>
#include <WiFi101.h>
우선, 프로그램 가장 윗부분엔 위와 같이 포함할 헤더파일을 나열해 줍니다. WiFi 모듈이 SPI 방식으로 연결되어 있고, WiFi101 라이브러리를 이용하기 때문에 위와 같이 입력합니다.
char ssid[] = "yourNetwork";
char pass[] = "secretPassword";
자신의 무선망에 접속하기 위해 SSID name과 Password 를 미리 입력해 줍니다.
int status = WL_IDLE_STATUS;
무선 모듈의 네트워크 연결 상태에 대한 상태값을 저장하기 위한 변수를 선언합니다. 이전 글에서 봤듯이 상태값이 WiFi101.h파일에 열거형으로 선언되어 있기 때문에, 정수형 변수를 선언해야 합니다.
char server[] = "api.openweathermap.org";
그 다음, 접속할 서버명을 지정합니다. server라는 이름의 char type 배열을 선언하고 문자열로 초기화 한 것이고, 이 코드 자체는 다른 의미가 없습니다.
서버 주소를 알기 위해선, 해당 사이트를 참고해야 합니다.
위 링크를 따라가시면 페이지 위쪽에 아래 그림처럼 API 요청 예제가 있습니다. 그냥 예제일 뿐이라서 저 사이트에서만 작동하고 다른 곳에서 사용할 수는 없지만 요청 메시지의 기본 구성을 확인할 수 있습니다.
api.openweathermap.org/data/2.5/forecast?id=524901&APPID=1111111111
위 주소에서 첫번째 슬래시(/)전까지가 호스트명이며 서버명입니다. 나머지 부분은 아래쪽에서 설명하겠습니다.
WiFiClient client;
WiFiClient 는 클래스 이름입니다. Client와 관련된 변수와 메소드를 하나로 묶어 놓았기 때문에, 우리는 위처럼 선언해서 사용하기만 하면 됩니다. 복잡한 부분은 라이브러리에 모두 미리 기술되어 있으니까요! client는 WiFiClient 클래스 타입의 클래스 변수로 선언되었고, 클라이언트와 관련된 기능들을 이 변수를 통해 쉽게 사용할 수 있습니다. 반대로, 만약, 아두이노로 서버를 구성한다면 미리 구현된 서버 클래스를 선언해서 사용할 것입니다.
void setup( ) { ... }
이제 setup()
함수를 구현할 차례인데, 아래와 같이 구성할 것입니다.
void setup() {
1. 사전 작업
2. 무선네트워크 연결
3. 서버 접속 및 요청
}
사전 작업은 아래와 같습니다.
//Configure pins for Adafruit ATWINC1500 Feather
WiFi.setPins(8,7,4,2);
//Initialize serial and wait for port to open:
Serial.begin(9600);
while (!Serial) {
; // wait for serial port to connect. Needed for native USB port only
}
// check for the presence of the shield:
if (WiFi.status() == WL_NO_SHIELD) {
Serial.println("WiFi shield not present");
// don't continue:
while (true);
}
이전 글에서 여러 번 말씀드린 것처럼, 무선 모듈을 사용하기 위해선 SPI 통신을 위한 핀 설정을 미리 해주어야 합니다. 그 부분이 setup()
함수의 처음에 위치했구요. 그 다음엔 결과 확인을 위한 Serial Monitor 설정이 나오고, 마지막으로 와이파이 쉴드 또는 와이파이 모듈이 정상적으로 연결되어 있는지 체크하는 코드가 나옵니다. 이 부분은 그대로 복사해서 붙이시면 됩니다.
참고로, if (WiFi.status() == WL_NO_SHIELD) {
이 부분에서 모듈 상태값을 체크하기 위해 WiFi.status()
함수를 사용합니다. 이 함수는 반환값(return value)으로 이전 글에서 봤었던 열거형 정수값을 사용합니다. 그리고, 위에서 미리 선언한 status
변수를 사용하지 않고 리턴값을 직접 if
문 조건 비교에 사용합니다. 따라서 status
는 값의 변화 없이 선언시에 초기화한 WL_IDLE_STATUS
값을 그대로 가지고 있습니다. 무선 모듈을 찾을 수 없다면(WL_NO_SHIELD
) 무한 루프에 빠지고 더이상 진행하지 않습니다.
무선 네트워크 연결
// attempt to connect to WiFi network:
while (status != WL_CONNECTED) {
Serial.print("Attempting to connect to SSID: ");
Serial.println(ssid);
status = WiFi.begin(ssid, pass);
// wait 2 seconds for connection:
delay(2000);
}
Serial.println("Connected to wifi");
status
가 WL_IDLE_STATUS
값이기 때문에 이 while
문은 실행됩니다. 그리고 WL_CONNECTED
가 뜰 때까지 즉, 연결이 설정될 때까지 무한루프를 돌게 됩니다. 연결 시도가 실패하면 2초간 기다린 후 다시 시도합니다. WiFi.begin()
메소드도 WiFi.status()
와 동일한 리턴값을 가집니다. 연결이 완성되면 while()
루프를 탈출하고 연결되었다는 메시지를 출력합니다. 여기까지는 이전 글에서 다 설명한 부분이고 다음 이어지는 부분을 자세히 확인하시기 바랍니다.
서버 접속 및 요청
Serial.println("\nStarting connection to server...");
if (client.connect(server, 80)) {
Serial.println("connected to server");
client.println("GET / HTTP/1.1");
client.println("Host: api.openweathermap.org");
client.println("Connection: close");
client.println();
}
client.connect()
이 함수를 통해 서버에 접속 합니다. 인수로 전달한 server
는 위에서 미리 지정한 서버명이고, 80
은 포트입니다. HTTP 서비스 대부분이 80포트를 이용하니 특별한 언급이 없다면 그대로 쓰면 됩니다. if
문 조건안에 있으므로 연결에 성공해야만 나머지를 수행하게 됩니다.
client.println("GET / HTTP/1.1");
client.println("Host: api.openweathermap.org");
client.println("Connection: close");
client.println();
if
문 안의 마지막 네줄이 위와 같습니다. 이 부분이 바로 서버에 데이터를 요청하는 부분입니다. 서버 요청시 헤더라고 부르는 곳인데, OpenWeatherMap에 대한 API 요청은 헤더만으로 구성됩니다.
client.println("GET / HTTP/1.1");
서버와의 연결이 설정되면, 서버로 데이터를 보내는 것은 간단합니다. 위와 같이 client.println()
함수를 사용하면 됩니다. GET / HTTP/1.1
이 부분이 서버로 전송되는 첫번째 줄인데, GET
은 메소드명입니다. 가장 많이 사용하는 메소드는 GET
과 POST
인데, GET
메소드는 주로 서버에 있는 데이터, 문서, 페이지 등을 요청할 때 많이 사용하고, POST
는 데이터를 전송하거나 서버에 있는 내용을 변경할 때 많이 사용합니다. OpenWeatherMap API는 GET
방식을 사용합니다. HTTP/1.1
은 HTTP 전송규약 버전 1.1을 사용하겠다는 뜻입니다. 의도적으로 변경해서 사용할 일은 거의 없으므로 그냥 그대로 옮기시면 됩니다.
GET / HTTP/1.1
GET
과 HTTP/1.1
사이에는 슬래시가 하나 있습니다. 이 부분이 서버에 요청하는 문서, 페이지, 데이터를 의미합니다. 슬래시(/) 하나만 있다면 이건 Root 즉 일반적인 홈페이지의 의미입니다. 기본적인 홈페이지 주소 즉 호스트명 외에 슬래시 이후로 이어진 부분이 저기에 들어갑니다.
HTTP 1.1 규약에는 호스트명(Host:)을 꼭 써야 하니, 위 그림처럼 Request 문을 작성하시면 됩니다.
client.println();
서버에 대한 Request 즉, 클라이언트의 요청시 헤더의 끝은 꼭 빈줄을 삽입해서 구분해야 합니다.
void loop() { ... }
이제 loop()
함수를 작성할 차례입니다. 이 프로그램은 무선 네트워크 접속 및 서버에 대한 요청이 한번만 이루어지므로 대부분의 코드를 setup()
함수에 작성하였습니다. 이제 남은 부분은 서버에서 전송시킨 데이터를 받아서 우리가 볼 수 있도록 출력해주는 코드입니다.
이 과정을 쉽게 얘기하면 클라이언트는 서버에서 전송된 데이터를 한 문자(byte)씩 읽어 냅니다. 전송 데이터는 여러 문자일테니, 한 문자씩 읽는다면 문자 수만큼 여러 번 읽어야 합니다. 데이터는 입력(수신) 대기열(buffer, queue)에 전송된 순서대로 쌓이고, 이 수신 버퍼는 역시 순서대로만 데이터를 빼낼 수 있습니다.
한 문자씩 읽어야 하고, 문자 수만큼 여러 번 읽어야 하므로, 이 부분은 반복문으로 처리해야 합니다. 몇 번 반복해야 할지 확실하지 않으므로 FOR
문 보다는 WHILE
문이 어울리겠죠!! 이전 글에서 본 Client예제도 while문으로 문자 수신 부분을 처리하고 있습니다.
그런데, 언제까지 읽어야 할까요? 전송 버퍼에 아무것도 없을 때까지? 만약, 서버에 의해서든, 전송 상태에 의해서든 문자가 시간 간격을 두고 도착한다면, 전송 버퍼가 비었다고 끝낼순 없습니다. 조금 뒤에 다른 데이터가 또 도착할테니까요! 그럼 전송이 끝났음을 어떻게 알 수 있을까요? 어렵지 않습니다. 웹 요청(request)은 기본적으로 전송이 완료되면 서버에서 접속을 끊습니다. 위에서 본 연결타입(Connection: close)이 close이면 전송이 완료된 후 바로 끊고, Keep-Alive라면 일정 시간 후에 접속을 끊습니다. 바로든 일정시간 후든 결국은 서버에서 접속을 끊기 때문에, 연결이 지속되고 있는지 아닌지 체크하면서 계속 입력을 받거나 대기하고 있으면 됩니다. 그래서, 문자 수신 코드는 setup()
함수 보다는 loop()
함수가 더 어울립니다.
loop()
함수는 아래와 같이 구성됩니다.
voide loop() {
1. 전송된 문자가 있다면 한 문자씩 읽어서 출력
2. 서버와의 연결이 끊겼는지 체크
}
전송된 문자가 있다면 한 문자씩 읽어서 출력한다.
available 이라는 단어의 뜻은 "가능한, 이용할 수 있는"입니다. WiFiClient
클래스에는 available()
함수가 있어서 전송 버퍼에 읽어올 문자가 있는지 여부를 체크합니다. 위에서 WiFiClient
클래스 변수로 client
를 선언했고, client.connect(server, 80)
코드를 이용해 서버에 연결했듯이, client.available()
코드를 이용하면 됩니다.
void loop() {
while ( client.available() ) {
...
...
}
}
위와 같은 구조로 작성할 수 있습니다. while() { ... }
을 이용해서 문자읽기를 반복합니다. 물론 available()
에 의해서 읽을 문자가 있을 때만 반복하고 없다면 즉, 전송 버퍼가 비었다면 while
문을 빠져 나옵니다. 빠져나온다해도 프로그램은 끝나지 않습니다. loop()
함수이기 때문에 계속 돌고 돕니다. 그러다가 다시 서버로부터 데이터가 전송되면 while
문으로 들어가서(client.available()
의 값이 참, true
가 되므로...) 문자 읽기를 반복합니다. 이 작업을 서버와의 접속이 끊길 때까지 한다고 했으니 while
문 아래쪽엔 접속 여부를 체크하는 코드를 작성합니다.
while (client.available()) {
char c = client.read();
Serial.write(c);
}
while
문을 완성했습니다. client.read()
함수는 전송 버퍼에서 한 문자(byte)를 읽어 옵니다. 동시에 버퍼에선 그 문자가 빠지겠죠! 읽어온 문자를 문자형(char type) 변수인 c
에 저장하고, 시리얼 모니터에 출력합니다.
서버와의 연결 상태 체크
if (!client.connected()) {
Serial.println();
Serial.println("disconnecting from server.");
client.stop();
// do nothing forevermore:
while (true);
}
loop()
함수 마지막에 서버 연결 상태를 체크합니다. client.connected()
함수를 이용하면 되는데, 연결중이라면 true
, 아니라면 false
를 리턴합니다. IF
문안의 조건식에서 느낌표(!)를 빼놓으면 안됩니다. 서버와의 연결이 끊겼다면 false
를 리턴하고 느낌표 즉 NOT 연산자에 의해서 false
를 true
로 변환해서 IF
문안에 들어가게 됩니다. 그러면, 메시지 출력하고 client.stop()
함수를 이용해 client
도 끝냅니다. 마지막으로 while (true);
구문을 통해 무한루프로 들어 가고 프로그램은 중단됩니다.
마지막으로 while (true);
구문을 통해 무한루프로 들어 가고 프로그램은 중단됩니다.
프로그램이 완성되었습니다. 전체 소스는 길이 문제로 글 아래쪽에 넣겠습니다. 이 프로그램은 OpenWeatherMap 사이트와의 연결 설정을 테스트하는 게 목적이라서 특정한 작업을 하진 않습니다. 그래서 출력된 데이터를 보면 html 양식의 에러 메시지가 있습니다. 정상적인 접근이 아닌 이유겠죠. 하지만, 이 소스에서 딱 한줄만 바꾸면 우리가 원하는 특정 도시의 날씨 정보를 받을 수 있으므로 프로그램은 모두 완성되었다고 보시면 됩니다. 생각보다 간단하죠?^^
Serial monitor에 출력된 결과 화면입니다. 1번은 프로그램이 출력한 메시지고, 2번은 서버에서 전송된 데이터중 헤더 부분입니다. 잘 보시면 헤더의 마지막 부분은 빈줄입니다. 빈줄이 오면 헤더가 끝났음을 알 수 있습니다. 3번은 전송된 본문입니다. Html 양식으로 되어 있고 역시 본문 마지막은 빈줄로 끝났고, 이 빈줄로 응답(response)이 끝났음을 알 수 있습니다. 다음에 서버 프로그램을 구성하실 때 이 부분을 그대로 적용하셔야 합니다.
마지막 4번은 프로그램이 출력한 메시지입니다. client.connected()
함수에 의해 서버와의 연결이 끝났음을 알고 출력한 것입니다. Connection: close 이 문장 때문에 전송을 완료하면 서버가 연결을 끊습니다. 그래서, 마지막 전송 데이터인 </html>
문장 다음에 빈줄이 출력되면 바로 저 메시지가 출력됩니다. 만약, Connection: close 구문을 삭제하거나 주석처리하고 다시 실행해 보시면, 전송이 완료된 후에도 disconnecting from server. 메시지가 출력되지 않다가 일정 시간이 지난후에 출력됩니다. Connection: 구문이 없으면 디폴트로 Keep-Alive로 설정되기 때문에, 전송이 완료된 후에도 바로 끊지 않고 일정 시간 대기후에 끊기 때문입니다.
이상입니다. 다음 글에선 실제 날씨 정보를 받아오는 부분을 설명하겠습니다.
전체 소스입니다.
#include <SPI.h>
#include <WiFi101.h>
char ssid[] = "TURTLE";
char pass[] = "yesican1";
int status = WL_IDLE_STATUS;
char server[] = "api.openweathermap.org";
WiFiClient client;
void setup() {
//Configure pins for Adafruit ATWINC1500 Feather
WiFi.setPins(8,7,4,2);
Serial.begin(9600);
while (!Serial) {
; // wait for serial port to connect. Needed for native USB port only
}
// check for the presence of the shield:
if (WiFi.status() == WL_NO_SHIELD) {
Serial.println("WiFi shield not present");
// don't continue:
while (true);
}
while (status != WL_CONNECTED) {
Serial.print("Attempting to connect to SSID: ");
Serial.println(ssid);
status = WiFi.begin(ssid, pass);
// wait 2 seconds for connection:
delay(2000);
}
Serial.println("Connected to wifi");
Serial.println("\nStarting connection to server...");
if (client.connect(server, 80)) {
Serial.println("connected to server");
client.println("GET / HTTP/1.1");
client.println("Host: api.openweathermap.org");
client.println("Connection: close");
client.println();
}
}
void loop() {
while (client.available()) {
char c = client.read();
Serial.write(c);
}
if (!client.connected()) {
Serial.println();
Serial.println("disconnecting from server.");
client.stop();
// do nothing forevermore:
while (true);
}
}