WiFi를 통한 아두이노 활용(12) : 날씨 정보 제공 #2

2017. 8. 3. 16:51

Arduino/Wireless

OpenWeatherMap로부터 선택한 도시의 날씨 정보를 받아 웹페이지로 제공

이전 글에서 만든 웹서버는 정보를 제공하기만 하고 별도의 입력은 받지 않았습니다. 이번 글에서는 클라이언트로부터 입력을 받아 처리하는 부분에 대해 다루어 보겠습니다.

이제까지 만들었던 오픈웨더맵 프로그램은 서울시의 날씨 정보를 보여주고 있습니다. 이를 수정하여 광역시까지 총 7개의 도시중 사용자가 선택한 도시의 날씨 데이터를 가져와 보여주는 프로그램으로 변경하겠습니다. 이를 위해서 두 가지 작업을 해야 합니다.

  • 7개의 도시 이름을 보여주고 사용자의 선택을 받는 양식(Form) 구성
  • 선택한 도시를 전송 받아 해당 도시의 날씨 데이터로 업데이트 하는 기능

첫 번째로, 입력 받기 위한 화면 구성부터 구현하겠습니다.

위 그림과 같이 HTML Form 요소인 <select> 태그를 이용하여 드롭다운 리스트 형태로 제공할 예정입니다.

폼은 Text Input, Button 처럼 사용자의 입력을 받기 위해 널리 사용되는 HTML 요소입니다. 익숙한 부분이라 바로 코드를 작성해 보겠습니다.

웹 문서의 한글 깨짐 방지

<meta charset="UTF-8">

폼 구성에 앞서 우선 위 태그를 삽입해야 줍니다. 도시명을 한글로 출력할 때 한글이 깨져서 나오는 문제를 해결할 수 있습니다. 태그는 헤더 부분에 위치하여 페이지 설정 등과 관련된 메타데이터를 제공합니다. 그래서 위치는 , 사이입니다. 태그안에는 태그에 대한 추가적인 데이터를 제공하기 위한 어트리뷰트가 존재할 수 있는데, charset이 바로 이에 해당합니다.

Attribute는 name = value 꼴로 사용되며 charset은 문서의 문자 인코딩 방식을 지정하는 어트리뷰트입니다. charset="UTF-8" character encoding으로 유니코드를 사용하겠다는 뜻입니다.

이 태그를 아두이노 코드로 삽입할 때 유의할 사항이 있습니다.

Serial.println(".............");
Client.println("...............");

Serial monitor에 문자를 출력하기 위해서 위 첫 번째 코드를 사용하는데, 출력할 내용이 큰 따옴표 사이에 오게 됩니다. 마찬가지로, 클라이언트에게 웹페이지를 전송하기 위해서 사용하는 두 번째 코드도 전송할 메시지가 큰 따옴표 사이에 옵니다. 즉, 큰 따옴표는 두 가지 경우 모두 출력할 문자열의 처음과 끝을 알려 줍니다.

Webclient.println("<meta charset="UTF-8">");

그래서, 위 메타 태그를 출력하기 위해서 위와 갈이 코드를 작성하면 에러가 날 수 밖에 없습니다. 출력하기 위해 중간에 삽입된 큰 따옴표와 구분이 안되기 때문이죠. 이를 방지하기 위해서 Escape문자 즉 제어문자가 필요합니다.

Webclient.println("<meta charset=\"UTF-8\">");

바로 역슬래시(\, 윈도우 환경에선 \)가 그 역할을 합니다. 위와 같이 출력할 따옴표 앞에 역슬래시를 붙여 주면 큰따옴표의 역할은 무시하고 출력만 하게 됩니다.

<meta charset="UTF-8">  // 큰 따옴표
<meta charset=UTF-8>  // 따옴표 생략
<meta charset='UTF-8'>  // 작은 따옴표

태그의 attribute에 값을 제공할 때 꼭 큰 따옴표만 사용 가능한 건 아닙니다. 위 세 가지 모두 유효한데, 아래 두 가지 코드는 이스케이프 문자도 필요 없어 좀더 간결하게 사용할 수 있습니다. attribute 값에 공백이 있거나 경우에 따라 따옴표를 생략하면 에러가 발생할 수 있음을 유의하기 바랍니다. 여기서는 큰 따옴표로 표기하겠습니다.

            Webclient.println("HTTP/1.1 200 OK");
            Webclient.println("Content-type:text/html");
            Webclient.println("Connection: close");
            Webclient.println();
            Webclient.println("<!DOCTYPE html>");
            Webclient.println("<html>");
            Webclient.println("<head>");
            Webclient.println("<meta charset=\"UTF-8\">");
            Webclient.println("<title>openweathermap Weather Info</title>");
            Webclient.println("</head>");
            Webclient.println("<body>");
            Webclient.println("<h1>Weather Info of City</h1>");

위와 같이 코드를 삽입해 줍니다.

Drop-Down List 작성

<form>
</form>

드롭다운 리스트 같은 폼 요소(Elements)들을 웹페이지에 삽입하기 위해서는 <form>, </form> 태그 안쪽에 위치시켜야 합니다.

<form>
  <select>
  </select>
</form>

<select>태그는 드롭다운 리스트를 만드는 태그입니다. 위와 같이 작성하면 웹페이지에서 drop-down list를 볼 수 있습니다. 단, 실행해 보면 선택할 항목은 없는 빈 리스트만 보게 됩니다.

<form>
  <select>
    <option>Content 1</option>
    <option>Content 2</option>
    <option>Content 3</option>
  </select>
</form>

<option>태그는 <select>태그안에서 선택할 옵션 즉 리스트를 제공합니다. 시작 태그<option>, 종료 태그</option> 사이에 오는 컨텐츠가 화면에 표시되는 항목 이름이 됩니다.

            Webclient.println("<!DOCTYPE html>");
            Webclient.println("<html>");
            Webclient.println("<head>");
            Webclient.println("<meta charset=\"UTF-8\">");
            Webclient.println("<title>openweathermap Weather Info</title>");
            Webclient.println("</head>");
            Webclient.println("<body>");
            Webclient.println("<h1>Weather Info of City</h1>");
            Webclient.println("<form>");
            Webclient.println("<select>");
            Webclient.println("<option>서울특별시</option>");
            Webclient.println("<option>부산광역시</option>");
            Webclient.println("<option>대구광역시</option>");
            Webclient.println("<option>인천광역시</option>");
            Webclient.println("<option>광주광역시</option>");
            Webclient.println("<option>대전광역시</option>");
            Webclient.println("<option>울산광역시</option>");
            Webclient.println("</select>");
            Webclient.println("</form>");
            Webclient.println("<p>Country : " + country + "</p>");
            Webclient.println("<p>City Name : " + cityName + "</p>");
            Webclient.println("<p>Longitude : " + coordLon + "</p>");
            Webclient.println("<p>Latitude : " + coordLat + "</p>");

위와 같이 코드 몇 줄만 삽입하여 원하는 폼 항목을 만들었습니다.

아이폰에서 접속하여 드롭다운 리스트를 터치한 모습입니다. 리스트를 표시하고 원하는 항목을 선택하는 것까지 확인했지만, 이는 모양만 만들었을 뿐, 내부적으로 어떠한 변경도 수행할 수 없습니다.

Webclient.println("<option value=\"1835847\">서울특별시</option>");

드롭다운 리스트에서 선택한 항목을 서버에게 전송하기 위해서 value라는 어트리뷰트가 필요합니다. value는 리스트에서 어떤 항목이 선택되었을 때 서버로 전송되는 값입니다. 위 코드는 서울특별시를 선택하면 1835847이라는 값이 서버로 전송된다는 의미입니다. 따라서, value attribute가 있어야만 서버에서 처리가 가능합니다.

1835847은 openweathermap API에서 사용하는 고유한 City ID 값입니다. 이제까지의 날씨 정보 요청 메시지에서 서울의 CityID값인 1835847을 사용하여 날씨 정보를 제공받았습니다. openweathermap.org에서 제공하는 도시 리스트를 참고하여 아래와 같이 각 광역시의 value값을 삽입했습니다.

            Webclient.println("<form>");
            Webclient.println("<select>");
            Webclient.println("<option value=\"1835847\">서울특별시</option>");
            Webclient.println("<option value=\"1838519\">부산광역시</option>");
            Webclient.println("<option value=\"1835327\">대구광역시</option>");
            Webclient.println("<option value=\"1843561\">인천광역시</option>");
            Webclient.println("<option value=\"1841808\">광주광역시</option>");
            Webclient.println("<option value=\"1835224\">대전광역시</option>");
            Webclient.println("<option value=\"1833742\">울산광역시</option>");
            Webclient.println("</select>");
            Webclient.println("</form>");

Submit Button 추가

이제 리스트에서 선택한 항목을 서버로 전송할 Input Button이 필요합니다.

<input>
<input type="submit">

폼에서 사용자의 입력 값을 받기 위해 사용하는 태그가 <input>입니다. 종료 태그(end tag)없이 사용하기 때문에 컨텐츠도 없고, 오직 attribute를 통해서 정의합니다. type attribute는 text, button, submit 등 input의 종류를 결정합니다. 사용자가 선택한 리스트 항목을 서버에 전송해야 하기 때문에 submit type으로 정의하였습니다.

            Webclient.println("<form>");
            Webclient.println("<select name=\"cityname\">");
            Webclient.println("<option value=\"1835847\">서울특별시</option>");
            Webclient.println("<option value=\"1838519\">부산광역시</option>");
            Webclient.println("<option value=\"1835327\">대구광역시</option>");
            Webclient.println("<option value=\"1843561\">인천광역시</option>");
            Webclient.println("<option value=\"1841808\">광주광역시</option>");
            Webclient.println("<option value=\"1835224\">대전광역시</option>");
            Webclient.println("<option value=\"1833742\">울산광역시</option>");
            Webclient.println("</select>");
            Webclient.println("<input type=\"submit\">");
            Webclient.println("</form>");

위와 같이 폼 태그안쪽에 submit(제출) 버튼을 설정하였습니다. 그리고, 추가적으로 <select>에 name attribute를 정의하였습니다. 이 값이 없으면 데이터가 서버로 전송되지 않습니다. submit 버튼은 <form>, </form> 태그 안쪽의 여러가지 폼요소들의 입력값을 모두 한번에 전송합니다. 따라서 어느 요소에 대한 값인지 구분이 가능해야 하기 때문에 요소마다 name값을 설정해 줍니다. 여기서는 1개 요소밖에 없어 한 가지 정보만 전송됩니다.

아래 전체 소스를 참고하세요!

#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 저장
String country = "";
String cityName = "";
String coordLon = "";
String coordLat = "";
String sunRise = "";
String sunSet = "";
String tempValue = "";
String humidValue = "";
String pressValue = "";
int status = WL_IDLE_STATUS;
char server[] = "api.openweathermap.org";
IPAddress ip;
WiFiServer Webserver(80); // Server 서비스를 위한 클래스 인스턴스
WiFiClient client;
void setup() {
  WiFi.setPins(8,7,4,2); 
  Serial.begin(9600);
  delay(1000);
  // 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");
  OpenWeatherMap();
  Webserver.begin();
  ip = WiFi.localIP();
  Serial.print("To see this page in action, open a browser to http://");
  Serial.println(ip);
}
void loop() {
  WiFiClient Webclient = Webserver.available();
  if (Webclient) {
    Serial.println("new client");
    bool newLine = false;
    while (Webclient.connected()) {
      if (Webclient.available()) {
        char c = Webclient.read();
        Serial.write(c);
        if (c == '\n') {
          if (newLine) {
            Serial.println("Client Request Ended!");
            Serial.println("Weather Data Request!");
            OpenWeatherMap(); 
            Serial.println("Web page transfer Start!");
            Webclient.println("HTTP/1.1 200 OK");
            Webclient.println("Content-type:text/html");
            Webclient.println("Connection: close");
            Webclient.println();
            Webclient.println("<!DOCTYPE html>");
            Webclient.println("<html>");
            Webclient.println("<head>");
            Webclient.println("<meta charset=\"UTF-8\">");
            Webclient.println("<title>openweathermap Weather Info</title>");
            Webclient.println("</head>");
            Webclient.println("<body>");
            Webclient.println("<h1>Weather Info of City</h1>");
            Webclient.println("<form>");
            Webclient.println("<select name=\"cityname\">");
            Webclient.println("<option value=\"1835847\">서울특별시</option>");
            Webclient.println("<option value=\"1838519\">부산광역시</option>");
            Webclient.println("<option value=\"1835327\">대구광역시</option>");
            Webclient.println("<option value=\"1843561\">인천광역시</option>");
            Webclient.println("<option value=\"1841808\">광주광역시</option>");
            Webclient.println("<option value=\"1835224\">대전광역시</option>");
            Webclient.println("<option value=\"1833742\">울산광역시</option>");
            Webclient.println("</select>");
            Webclient.println("<input type=\"submit\">");
            Webclient.println("</form>");
            Webclient.println("<p>Country : " + country + "</p>");
            Webclient.println("<p>City Name : " + cityName + "</p>");
            Webclient.println("<p>Longitude : " + coordLon + "</p>");
            Webclient.println("<p>Latitude : " + coordLat + "</p>");
            Webclient.println("<p>Sun Rise : " + sunRise + "</p>");
            Webclient.println("<p>Sun Set : " + sunSet + "</p>");
            Webclient.println("<p>Temperature : " + tempValue + "</p>");
            Webclient.println("<p>Humidity : " + humidValue + "</p>");
            Webclient.println("<p>Pressure : " + pressValue + "</p>");
            Webclient.println("</body>");
            Webclient.println("</html>");
            break;
          } else {
            newLine = true;
          }
        } else if (c != '\r') {
          newLine = false;
        }
      }
    } 
    Webclient.stop();
    Serial.println("client disonnected");
  }
}
void OpenWeatherMap() { 
  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();
  } 
  while (client.connected()) {
    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; 
          if (startTag.indexOf("country") != -1) {
            if (endTag.indexOf("country") != -1) {
              Serial.print("Country : ");
              Serial.println(currentData);
              country = currentData;
            }
          }
          currentData = "";
        } else {
          flagStartTag = true;
          startTag = currentTag;
          startTagProcessing();
        }
        currentTag = "";
      }
    }
  }
  Serial.println();
  Serial.println("disconnecting from server.");
  client.stop();
}
void startTagProcessing() {
  if (startTag.startsWith("<city")) {
    int attribName = startTag.indexOf("name=");
    if (attribName != -1) {
      cityName = startTag.substring(attribName + 6);
      int quote = cityName.indexOf("\"");
      cityName = cityName.substring(0, quote);
      Serial.println("City : " + cityName);
    }
  } else if (startTag.startsWith("<coord")) {
    int attribLon = startTag.indexOf("lon=");
    if (attribLon != -1) {
      coordLon = startTag.substring(attribLon + 5);
      int quote = coordLon.indexOf("\"");
      coordLon = coordLon.substring(0, quote);
      Serial.println("Longitude : " + coordLon);
    }
    int attribLat = startTag.indexOf("lat=");
    if (attribLat != -1) {
      coordLat = startTag.substring(attribLat + 5);
      int quote = coordLat.indexOf("\"");
      coordLat = coordLat.substring(0, quote);
      Serial.println("Latitude : " + coordLat);
    }
  } else if (startTag.startsWith("<sun")) {
    int attribTime = startTag.indexOf("rise=");
    if (attribTime != -1) {
      String riseHour = startTag.substring(attribTime + 17, attribTime + 19);
      String riseMin = startTag.substring(attribTime + 20, attribTime + 22);
      int tempValue = riseHour.toInt();
      tempValue -= 15;
      riseHour = String(tempValue);
      Serial.println("Sun Rise : " + riseHour + ":" + riseMin);
      sunRise = riseHour + ":" + riseMin;
    }
    attribTime = startTag.indexOf("set=");
    if (attribTime != -1) {
      String setHour = startTag.substring(attribTime + 16, attribTime + 18);
      String setMin = startTag.substring(attribTime + 19, attribTime + 21);
      int tempValue = setHour.toInt();
      tempValue += 9;
      setHour = String(tempValue);
      Serial.println("Sun Set : " + setHour + ":" + setMin);
      sunSet = setHour + ":" + setMin;
    }
  } else if (startTag.startsWith("<temperature")) {
    int attribValue = startTag.indexOf("value=");
    if (attribValue != -1) {
      tempValue = startTag.substring(attribValue + 7);
      int quote = tempValue.indexOf("\"");
      tempValue = tempValue.substring(0, quote);
      Serial.println("Temperature : " + tempValue);
    }
  } else if (startTag.startsWith("<humidity")) {
    int attribValue = startTag.indexOf("value=");
    if (attribValue != -1) {
      humidValue = startTag.substring(attribValue + 7);
      int quote = humidValue.indexOf("\"");
      humidValue = humidValue.substring(0, quote);
      Serial.println("Humidity : " + humidValue + "%");
    }
  }else if (startTag.startsWith("<pressure")) { 
    int attribValue = startTag.indexOf("value=");
    if (attribValue != -1) {
      pressValue = startTag.substring(attribValue + 7);
      int quote = pressValue.indexOf("\"");
      pressValue = pressValue.substring(0, quote);
      Serial.println("Pressure : " + pressValue + " hPa");
    }
  }
}

여기까지 작성 후 실행해 보면 아래와 같이 결과를 확인 할 수 있습니다.

그리고, 위 화면의 노란색 부분은 웹페이지에서 "부산광역시"를 선택했을 때 클라이언트(아이폰의 웹브라우저)로부터 전송받은 Request message입니다.

GET /?cityname=1838519 HTTP/1.1
Host: 192.168.1.65
Connection: keep-alive

위 내용은 클라이언트로 날씨 정보 받아오는 부분에서 이미 다루었던 내용으로 GET 메소드를 이용하여 클라이언트로부터 사용자 입력값을 전송 받는 모습입니다. <Form>은 별도로 지정하지 않으면 GET 방식으로 데이터를 전송합니다. "/" 뒤에 요청 내용이 오고, "?"뒤에 폼 데이터가 나열됩니다. 각각의 폼 요소 입력값은 "&"로 구분하지만 여기서는 하나의 입력값 밖에 없으므로 확인할 수 없습니다.

cityname은 도시 리스트를 나타내는 <select>태그의 name value이고, 그 뒤에 부산의 City ID값인 value attribute값이 나옵니다. 서버쪽에서 이 요청 메시지를 분석하여 처리하도록 코드를 구성하면 됩니다.

GET 메시지를 서버쪽에서 처리하는 부분은 길어질 듯 하여 다음 글로 넘기고, <meta>태그 하나 추가하며 이번 글을 마무리 하겠습니다.

이번 글에서 작성한 예제를 실행한 후, 아이폰의 사파리를 통해 접속해 보면 위 왼쪽 화면과 같이 작게 보입니다. 이를 해결하기 위해 메타 태그 하나를 추가하여 접속한 디바이스의 화면 크기에 따라 컨텐츠 사이즈를 조절할 수 있는데, 오른쪽 화면이 그 결과입니다.

<meta name="viewport" content="width=device-width, initial-scale=1.0">

위 태그가 바로 디바이스 크기에 따라 화면 크기를 조절하는 반응형(Responsive) 웹페이지를 제공하는 기능을 합니다. width는 화면 크기이며 device-width 즉 디바이스의 화면 크기를 따르도록 합니다. initial-scale은 처음 접속시 줌 비율을 말합니다.

Webclient.println("<!DOCTYPE html>");
Webclient.println("<html>");
Webclient.println("<head>");
Webclient.println("<meta charset=\"UTF-8\">");
Webclient.println("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
Webclient.println("<title>openweathermap Weather Info</title>");
Webclient.println("</head>");

아두이노를 웹서버로 구축할 경우, 스마트폰을 이용해서 접근하는 경우가 많은 듯하여 미리 추가해 보았습니다.

이상입니다.

Comments