아두이노와 무선인터넷을 통해 날씨 정보 가져오기 세번째
이전 글에서, 날씨 정보 사이트인 OpenWeatherMap에서 제공되는 API를 통해 날씨 정보를 받아오는 프로그램을 완성하였습니다. 이번 글에서는 몇가지 추가적인 parameter 에 대해 소개하고, 전송받은 날씨 데이터에서 원하는 부분을 추출하는 코드를 작성하겠습니다.
OpenWeatherMap 의 추가적인 API 파라미터 소개
원하는 단위 선택해서 받기
섭씨, 화씨, 캘빈값 선택해서 받을 수 있습니다. units 라는 파라미터를 이용하고 아래와 같이 붙여주시면 됩니다.
Units format
- Standard : Kelvin, 디폴트값으로 생략 가능
- Metric : Celsius
- Imperial : Fahrenheit
아래 링크로 가시면 각 포맷에 따른 상세한 표시 단위를 표로 보실 수 있습니다.
우리 나라는 metric 값을 이용하시면 됩니다. 사용법은 아래와 같습니다.
api.openweathermap.org/data/2.5/forecast?id=524901&APPID=1111111111&units=metric
api.openweathermap.org/data/2.5/forecast?id=524901&APPID=1111111111&units=imperial
원하는 포맷으로 선택해서 받기
이전 글에서 봤던 것처럼, 별다른 언급이 없으면 디폴트로 JSON 포맷으로 응답합니다. 파라미터 값에 따라서 XML, HTML 중에 골라서 받을 수 있고, 아래와 같이 사용하시면 됩니다.
api.openweathermap.org/data/2.5/forecast?id=524901&APPID=1111111111&mode=xml
api.openweathermap.org/data/2.5/forecast?id=524901&APPID=1111111111&mode=html
JSON, XML, HTML 중에 아두이노로 처리하기 쉬운 건 XML 이라 생각합니다. 세가지 포맷은 우선 "파싱"이라는 과정을 거쳐 우리가 원하는 데이터를 추출해야 하는데, arduino에서 그나마 XML의 파싱이 좀더 쉬운 것 같습니다. 셋 다 복합하긴 하지만....! HTML은 데스크탑이나 스마트폰 위젯으로 더 어울릴 듯 하네요! 저는 XML로 받아서 처리하겠습니다.
GET /data/2.5/weather?id=1835847&APPID=7b18dfe35b4273bbe63654d75573cacf&units=metric&mode=xml HTTP/1.1
최종적인 API 요청 구문은 위와 같습니다. 그리고, 아래 그림이 그 결과값입니다.
참고로, 이전에 언급한 바와 같이 아래 예처럼 호스트명과 API Call 구문을 합치면 HTTP 주소가 되고, 이 주소로 웹브라우저에서도 결과를 확인할 수 있습니다.
http://api.openweathermap.org/data/2.5/weather?id=1835847&APPID=7b18dfe35b4273bbe63654d75573cacf&mode=xml&units=metric
위와 같이 결과를 볼 수 있는데, arduino에서 본 결과와는 약간 다릅니다. 물론 보여지는 모습만 약간 다르고 내용은 같습니다.
XML 데이터 분석 및 처리 : 파싱
아두이노 시리얼 모니터에 보이는 결과는 딱 두 줄입니다. 첫째 줄은 매우 짧고, 두번 째 줄은 매우 길게 늘어져 있죠! 복사해서 붙여 보면 아래와 같습니다.
<?xml version="1.0" encoding="UTF-8"?>
<current><city id="1835847" name="Seoul-teukbyeolsi"><coord lon="127" lat="37.58"></coord><country>KR</country><sun rise="2017-07-18T20:26:00" set="2017-07-19T10:50:18"></sun></city><temperature value="24.8" min="24" max="26" unit="metric"></temperature><humidity value="88" unit="%"></humidity><pressure value="1007" unit="hPa"></pressure><wind><speed value="1.5" name="Calm"></speed><gusts></gusts><direction value="360" code="" name=""></direction></wind><clouds value="75" name="broken clouds"></clouds><visibility value="3500"></visibility><precipitation mode="no"></precipitation><weather number="701" value="mist" icon="50n"></weather><lastupdate value="2017-07-19T21:00:00"></lastupdate></current>
<?xml version="1.0" encoding="UTF-8"?>
첫째 줄입니다. 프롤로그 라고 하는데, 버전과 인코딩 관련 정보가 들어 있습니다. UTF-8은 XML 기본 인코딩이고, 이 Prolog는 생략 가능합니다.
<current>
<city id="1835847" name="Seoul-teukbyeolsi">
<coord lon="127" lat="37.58"></coord>
<country>KR</country>
<sun rise="2017-07-18T20:26:00" set="2017-07-19T10:50:18"></sun>
</city>
<temperature value="24.8" min="24" max="26" unit="metric"></temperature>
<humidity value="88" unit="%"></humidity>
<pressure value="1007" unit="hPa"></pressure>
<wind>
<speed value="1.5" name="Calm"></speed>
<gusts></gusts>
<direction value="360" code="" name=""></direction>
</wind>
<clouds value="75" name="broken clouds"></clouds>
<visibility value="3500"></visibility>
<precipitation mode="no"></precipitation>
<weather number="701" value="mist" icon="50n"></weather>
<lastupdate value="2017-07-19T21:00:00"></lastupdate>
</current>
둘째 줄이 본문이고, 위와 같이 줄바꿈과 탭을 이용하여 계층적으로 정리했습니다. 계층 구조는 XML의 대표적인 특징 중 하나입니다.
XML의 기본 구조 : Elements
위 XML 응답메시지의 네번째 줄을 보시면 아래와 같습니다.
<country>KR</country>
이 한줄을 Elements라고 합니다. 하나의 엘리먼츠는 <start tag>로 시작해서 <end tag>로 끝납니다. HTML 처럼 "<" 와 ">"로 묶여진 부분을 Tag라고 하며 <country> 는 start tag, </country>는 end tag입니다. XML은 HTML과 달리 꼭 end tag로 Closing 되어야 합니다.
KR은 Contents 입니다. 위와 같이 Text로 된 컨텐츠가 올 수도 있고, 또 다른 엘리먼츠가 컨텐츠로 올 수도 있으며 그냥 비어 있는 Empty Elements도 가능합니다. OpenWeatherMap의 API 응답 메시지도 대부분 Empty Elements로 구성되었네요!
<wind>
<speed value="1.5" name="Calm"></speed>
<gusts></gusts>
<direction value="360" code="" name=""></direction>
</wind>
위 XML Elements는 start tag <wind>와 end tag </wind> 로 구성되어 있고, 컨텐츠는 텍스트가 아니라 또 다른 엘리먼츠 speed, gusts, direction으로 구성되어 있습니다.
<speed value="1.5" name="Calm"></speed>
위 엘리먼츠는 start tag 와 end tag로만 구성되었고 컨텐츠 부분은 비어 있는 Empty Elements 입니다. 이 엘리먼츠는 start tag 내에 value와 name이라는 데이터를 포함하고 있는데, 이를 attributes 라고 합니다. 어트리뷰츠는 꼭 큰 따옴표로 묶어 주어야 합니다. 위 엘리먼츠는 아래와 의미가 같습니다.
<speed value="1.5" name="Calm" />
웹 브라우저에 실행한 결과 보시면 위와 같이 표현되어 있습니다.
XML은 태그 이름이 정해져 있지 않습니다. 이 부분은 HTML과 다른데, 사용자가 태그를 원하는 대로 만들어서 데이터를 보낼 수 있어 편리한 부분도 있지만, 받는 쪽에선 매번 그에 맞는 코딩을 해야 하는 번거로움이 있긴 합니다. 나중에 아이폰과 데이터를 주고 받는 연습도 해 볼 예정인데, 마음대로 태그를 만들어서 보내면 된다는 점이 편하긴 합니다.
응답 메시지에서 본문 즉, 태그만 구별 하기
응답 메시지에서 프로그램에서 필요한 건 본문 즉, 태그 부분입니다. 우선 그 부분만 구별해서 출력하는 연습을 해보겠습니다.
while (client.available()) {
char c = client.read();
Serial.write(c);
}
이 부분이 이전 글에서 봤던 소스 중, 서버로부터 받은 데이터를 한 문자씩 시리얼 모니터에 출력하는 부분입니다. 이 부분을 고쳐서 쓰면 됩니다.
우선 태그만 인식해서 출력하는 프로그램을 작성하겠습니다. 태그만 인식하므로, 태그 밖에 있는 문자는 출력하지 않습니다.
당연히, 태그 사이에 있는 텍스트 컨텐츠도 마찬가지구요. 즉, country 엘리먼츠의 "KR" 텍스트는 태그 밖에 있으므로 건너 뛰게 됩니다. 태그는 "<"로 시작해서 ">"로 끝나므로 IF문으로 간단하게 체크 가능할 겁니다.
Bool tagInside = false; // 태그 안쪽인지 바깥쪽인지 구별하는 변수
결국, 응답 메시지는 '태그 안이냐, 밖이냐' 두가지 영역중에 하나입니다. 그래서, flag역할을 할 Bool(Boolean) type의 변수 tagInside를 하나 선언합니다. 선언 위치는 setup() 함수 위쪽에 두어 전역변수로 사용합니다. tagInside 가 true 이면 태그 안쪽이니 출력하고, false 이면 태그 바깥쪽이니 출력하지 않습니다.
"<"기호를 만나면 태그가 시작되므로 tagInside에 true값을 주고, ">"기호를 만나면 tagInside를 false로 놓습니다.
//태그일때만 출력
while (client.available()) {
char c = client.read();
if (c == '<') {
tagInside = true;
}
if (tagInside) {
Serial.write(c);
}
if (c == '>') {
tagInside = false;
}
}
결과 화면입니다. 프로그램 자체에서 출력한 메시지 외에, 서버에서 받은 메시지는 태그만 남고 모두 출력되지 않았습니다. 그림에선 안보이지만, <country> 태그 사이의 텍스트 KR 도 역시 없습니다. 전체 소스는 바로 밑에서 확인하세요!
#include <SPI.h>
#include <WiFi101.h>
char ssid[] = "TURTLE";
char pass[] = "yesican1";
bool tagInside = false; // 태그 안쪽인지 바깥쪽인지 구별하는 변수
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 /data/2.5/weather?id=1835847&APPID=7b18dfe35b4273bbe63654d75573cacf&units=metric&mode=xml HTTP/1.1");
client.println("Host: api.openweathermap.org");
client.println("Connection: close");
client.println();
}
}
void loop() {
//태그 안 일때만 출력
while (client.available()) {
char c = client.read();
if (c == '<') {
tagInside = true;
}
if (tagInside) {
Serial.write(c);
}
if (c == '>') {
tagInside = false;
}
}
if (!client.connected()) {
Serial.println();
Serial.println("disconnecting from server.");
client.stop();
// do nothing forevermore:
while (true);
}
}
태그를 저장하여 사용하기
이번에는 바로 출력하지 않고 저장한 후에 출력하겠습니다. 처리하려면 우선 변수에 저장해서 사용하는게 유리하니까요! 입력 받은 태그를 저장하기 위한 String 형 변수 currentTag를 아래와 같이 선언합니다.
Bool tagInside = false; // 태그 안쪽인지 바깥쪽인지 구별하는 변수
String currentTag = ""; // 현재 태그를 저장하기 위한 변수, 공백으로 초기화
그리고, 입력 처리 부분을 아래와 같이 변경하였습니다.
while (client.available()) {
char c = client.read();
if (c == '<') {
tagInside = true;
}
if (tagInside) {
currentTag += c;
}
if (c == '>') {
tagInside = false;
Serial.println(currentTag);
currentTag = "";
}
}
두번째 IF문에서 tagInside 가 true 일때 문자를 출력하는 것이 아니라 currentTag 변수에 저장하도록 변경하였습니다. 그리고 태그가 끝나면, 이제까지 저장한 현재 태그를 출력하고 currentTag 는 새로운 태그를 저장하기 위해 초기화 합니다. println() 함수를 사용했기에 태그가 한 라인에 하나씩 보기 좋게 출력됩니다.
결과 화면입니다.
START TAG 와 END TAG 의 구별
이어서 start tag(<>)와 end tag(</>)를 구별하는 코드를 작성하겠습니다. 태그 닫힘기호(">")가 들어오면 태그가 하나 완성되었으니 IF문으로 판별합니다. "</"기호로 시작하면 end tag이고 아니라면 start tag이므로 쉽게 구별 가능합니다.
if (c == '>') {
tagInside = false;
if (currentTag.startsWith("</")) {
Serial.print("End Tag : ");
} else {
Serial.print("Start Tag : ");
}
Serial.println(currentTag);
currentTag = "";
}
end tag 인지 확인하는 IF문의 조건문을 보시면 .startsWith()
함수가 사용되고 있습니다. 이는 문자열을 저장하는 Object type 인 String type의 멤버 함수로서 문자열이 괄호안의 인수로 시작되는 지를 체크합니다. 일치한다면 true, 아니면 false를 반환하므로, 위 IF문이 참이면 end tag, 거짓이면 start tag가 됩니다. (기존 C언어의 문자열 type인 소문자 string과 구별하세요!!) 아래 링크로 가시면 다른 멤버 함수들도 참고하실 수 있습니다. 이 프로그램에서도 몇가지 String 멤버 함수를 더 사용할 예정입니다.
결과 화면입니다. 잘 구별하고 있네요!^^
TEXT Contents 저장하기
이제 country 태그의 KR 처럼 태그 사이의 데이터 즉 텍스트 컨텐츠를 저장하는 코드 차례입니다.
bool tagInside = false; // 태그 안쪽인지 바깥쪽인지 구별하는 변수
String currentTag = ""; // 현재 태그를 저장하기 위한 변수, 공백으로 초기화함
String currentData = ""; // 태그 사이의 컨텐츠를 저장하는 변수
우선 컨텐츠 데이터를 저장할 변수를 위와 같이 선언합니다. 역시 공백("")으로 초기화 하구요! 데이터를 저장할 시점은 아래와 같습니다.
- tagInside = false; 즉, start tag, end tag 안쪽이면 안됩니다. 바깥쪽인지 체크.
- 데이터는 start tag가 끝난 후 부터 저장합니다. start tag 인지 체크.
">"기호를 만나 태그가 완성될 때마다 start tag인지 end tag인지 구별하는 변수를 하나 추가합니다.
bool tagInside = false; // 태그 안쪽인지 바깥쪽인지 구별하는 변수
bool flagStartTag = false; // 스타트 태그인지 구별하는 변수
String currentTag = ""; // 현재 태그를 저장하기 위한 변수, 공백으로 초기화함
String currentData = ""; // 태그 사이의 컨텐츠를 저장하는 변수
if (tagInside) {
currentTag += c;
} else if (flagStartTag) {
currentData += c;
}
while()문 안쪽의 두번째 IF문을 위와 같이 수정했습니다. 위에서 말한 두가지 조건 즉, 태그 안쪽이 아닐 때 그리고 스타트 태그가 나온 후에 데이터를 저장합니다.
if (c == '>') {
tagInside = false;
if (currentTag.startsWith("</")) {
flagStartTag = false;
} else {
flagStartTag = true;
}
Serial.println(currentTag);
currentTag = "";
}
세 번째 IF문도 위와 같이 수정하였습니다. ">" 기호를 만나서 태그가 완료되었음을 확인 했을 때, 현재 완료된 태그가 end Tag 이면("</"로 시작하면...) flagStartTag는 false로, start Tag이면 true로 값을 변경합니다.
여기에 추가하여,
이 글 위쪽에서 XML의 Elements 개념에 대해서 언급했었습니다. start tag, contents, end tag 가 모여서 하나의 Elements 를 이루고 이 엘리먼츠가 한 단위로 처리됩니다. 그래서, start tag와 end tag를 따로 저장하고 있는 게 유용할 수 있습니다.
bool tagInside = false; // 태그 안쪽인지 바깥쪽인지 구별하는 변수
bool flagStartTag = false; // 스타트 태그인지 구별하는 변수
String currentTag = ""; // 현재 태그를 저장하기 위한 변수, 공백으로 초기화함
String currentData = ""; // 태그 사이의 컨텐츠를 저장하는 변수
String startTag = ""; // 현재 elements의 start tag 저장
String endTag = ""; // 현재 elements의 end tag 저장
위와 같이 두 개의 변수를 추가하였습니다.
if (c == '>') {
tagInside = false;
if (currentTag.startsWith("</")) {
flagStartTag = false;
endTag = currentTag; // store endTag
} else {
flagStartTag = true;
startTag = currentTag; // store startTag
}
Serial.println(currentTag);
currentTag = "";
}
start, end 태그를 구별하는 부분에서 각각 저장까지 하도록 수정했습니다. 이제 잘 작동하는지 확인할 겸, 결과를 출력하는 코드를 추가하겠습니다.
if (c == '>') {
tagInside = false;
if (currentTag.startsWith("</")) {
flagStartTag = false;
endTag = currentTag; // store end Tag
Serial.print(currentData);
Serial.println(endTag);
currentData = "";
} else {
if (flagStartTag) {
Serial.println("");
} else {
flagStartTag = true;
}
startTag = currentTag; // store start Tag
Serial.print(startTag);
startTag = "";
}
currentTag = "";
}
약간 길어졌는데, 내용은 쉽습니다. 태그가 완성되고 나서 스타트 태그라면 startTag 값을 출력합니다. 이때는 줄을 바꾸지 않습니다. 한 엘리먼츠가 한 줄에 출력되도록 하고 싶어서 입니다. 태그가 완성되고 엔드 태그라면 컨텐츠 데이터(currentData)를 출력하고 endTag 까지 출력하며 이 때 한 엘리먼츠가 끝났으므로 줄이 바뀝니다(println) . currentData는 출력된 후엔 더이상 사용할 일이 없기 때문에 다음 태그값을 받기 위해 공백("")으로 초기화 해줍니다.
위 소스에서 아래 코드 부분을 보시면,
if (flagStartTag) {
Serial.println("");
} else {
flagStartTag = true;
}
이건 스타트 태그가 연달아 올 때는 계층적으로 보기 좋게 하기위해 줄을 바꿔주기 위함입니다. 단지 시리얼 모니터에 출력할 때 모양 내려는 의도이고 실제 데이터 처리와는 관련 없습니다. 원래대로 flagStartTag = true;
이 한 줄만 쓰셔도 무방합니다.
결과 화면입니다. XML 태그만 Elements 단위로 잘 출력되었습니다.
여기까지 XML 파싱(Parsing)에 대해서 접근해 봤습니다. 실제 데이터 처리를 통한 원하는 정보를 얻는 부분은 없지만, 그 준비는 다 마쳤으므로 다음 글에서 마지막으로 정리하도록 하겠습니다.
이상입니다. (전체 소스는 바로 밑에 있습니다.)
#include <SPI.h>
#include <WiFi101.h>
char ssid[] = "TURTLE";
char pass[] = "yesican1";
bool tagInside = false; // 태그 안쪽인지 바깥쪽인지 구별하는 변수
bool flagStartTag = false; // 스타트 태그인지 구별하는 변수
String currentTag = ""; // 현재 태그를 저장하기 위한 변수, 공백으로 초기화함
String currentData = ""; // 태그 사이의 컨텐츠를 저장하는 변수
String startTag = ""; // 현재 elements의 start tag 저장
String endTag = ""; // 현재 elements의 end tag 저장
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 /data/2.5/weather?id=1835847&APPID=7b08dfe35b4273bbe63604c75573cacf&units=metric&mode=xml HTTP/1.1");
client.println("Host: api.openweathermap.org");
client.println("Connection: close");
client.println();
}
}
void loop() {
while (client.available()) {
char c = client.read();
if (c == '<') {
tagInside = true;
}
if (tagInside) {
currentTag += c;
} else if (flagStartTag) {
currentData += c;
}
if (c == '>') {
tagInside = false;
if (currentTag.startsWith("</")) {
flagStartTag = false;
endTag = currentTag; // store end Tag
Serial.print(currentData);
Serial.println(endTag);
currentData = "";
} else {
if (flagStartTag) {
Serial.println("");
} else {
flagStartTag = true;
}
startTag = currentTag; // store start Tag
Serial.print(startTag);
startTag = "";
}
currentTag = "";
}
}
if (!client.connected()) {
Serial.println();
Serial.println("disconnecting from server.");
client.stop();
// do nothing forevermore:
while (true);
}
}