반응형

이전 글 아두이노 - ESP01 모듈, 기상청 RSS / 오픈웨더맵 API 날씨 정보 받기 에서 JSON 형식의 날씨 정보를 받아 JSON 라이브러리를 통해 원하는 값을 추출하고자 하였으나 JSON 라이브러리 예제에서는 잘 되던 것이 오픈 웨더 맵을 통해 받은 데이터에서는 원하는 값을 추출 할 수 없었다.  수신한 날씨 정보 데이터가 라이브러리 예제 처럼 단순히 배열의 나열이 아닌, 배열에 객체가 포함되어 있어 JSON 라이브러리를 통해 parsing 하지 못했을 수도 있으나 정확한 원인을 찾지는 못했었다. 하지만 JSON 형식으로 들어오는 데이터 역시 스트링이므로 스트링 클래스 함수를 이용하여 원하는 값을 추출했었는데 여기에서 더 나아가 JSON 형식의 데이터 규칙을 활용한 사용자 함수를 만들고 그 함수를 이용해 JSON 라이브러리와 비슷한 방식으로 쉽게 추출할 수 있도록 parsing 코드를 만들어 보자.  

 

{"coord":{"lon":126.98,"lat":37.57},"weather":[{"id":701,"main":"Mist","description":"mist","icon":"50n"}],"base":"stations","main":{"temp":283.7,"pressure":1020,"humidity":100,"temp_min":281.15,"temp_max":286.15},"visibility":1200,"wind":{"speed":0.5,"deg":40},"clouds":{"all":75},"dt":1572715825,"sys":{"type":1,"id":5501,"country":"KR","sunrise":1572731897,"sunset":1572769974},"timezone":32400,"id":1835848,"name":"Seoul","cod":200} 

* 오픈웨더맵에서 보내는 JSON 형식의 날씨 정보에서 소수값의 경우에 그 값의 표현에 항상 점('.')이 포함되어 있는 것은 아니다. 온도의 값을 살펴보면 17도 일때 17.00으로 표시되지 않고 정수로 17만 표시한다. 소수점 아래 자리에 값이 있는 경우에만 점('.')을 사용하여 17.2처럼 표시하므로 주의를 필요로 한다.   

 

상기 JSON 형식의 날씨 정보는 아두이노로 오픈 웨더 맵에서 받은 것이다. JSON 문서에의하면 JSON 형식은 임베디드 시스템에 적합한 데이터 구조라고 얘기를 하고 있다. 이 얘기는 각 데이터를 표현하는 데 있어서 특정한 규칙을 갖고 있고 그 규칙에 맞게 스트링 형식(char 문자열)으로 전송을 한다는 것이다. JSON 문서를 살펴보면 {}의 의미, 객체 표현방법 등이 서술되어 있지만 JSON 형식의 데이터를 parsing 할 때에는 그러한 것들을 생각하지 말고 우선 위의 데이터를 살펴보자. 

 

첫 번째, "스트링"처럼 큰 따옴표 기호(")로 묶인 스트링은 변수 명이고 고유하다.   

두 번째, 변수에 대한 값은 연이어 나오는':'문자 다음에 위치한다. 

세 번째, 값이 스트링이면 큰 따옴표(")로 묶이고, 값이 소수이거나 정수이면 큰 따옴표(")가 없으며 값 다음에 반드시 콤마(,)가 있다.

 

이 세 가지 기준으로 분류를 하게 되면 JSON 라이브러리 없이도 JSON 형식의 데이터를 추출하는데 문제가 없게 된다. 

 

다만 아두이노에서 테스트를 하기 위해서 String 변수에 상기의 JSON 형식의 데이터를 바로 넣을 수가 없다. String 형식이라고 지정해주는 인자는 큰 따옴표로 묶어주는 것("스트링")이다.  JSON 형식 데이터를 스트링 변수에 저장하기 위해서는 아래와같이 JSON 형식 데이터를 큰 따옴표로 묶어주어야만 한다. 

String line = "JSON 형식 데이터";

하지만, JSON 형식 데이터 역시 데이터 내의 스트링을 표시하기 위해 큰 따옴표(")를 사용하고 있어서 정상적으로 저장할 수가 없게 된다. 만약 { "Sunday": 0 } 형식의 JSON 데이터를 테스트하고자 한다면

String line = "{ "Sunday": 0 }";

상기처럼 될 것이지만 큰 따옴표를 기준으로 살펴보면 "{ ", ": 0 }" 두 개의 스트링이 있다고 표현된 것으로 바뀌게 되고 Sunday는 스트링이 아닌 게 되어 버려 컴파일 오류가 발생하게 된다.   

String line = "{ " ": 0 }"; 

상기처럼 Sunday를 없애주면 오류 없이 컴파일된다. 이 오류를 없애기 위해서는 JSON 형식 데이터 안의 큰따옴표가 묶이지 않도록 해주게 되면 된다. 하지만 스트링을 출력할 때에는 정상적인 JSON 형식을 유지해야 한다. JSON 라이브러리의 예제를 살펴보면 테스트용 JSON 데이터를 String 변수에 저장할 때 역 슬레쉬(backslash) 문자를 사용한 것을 확인할 수 있다.  

String line = "{ \"Sunday\": 0 }";

JSON 형식 데이터 내의 큰 따옴표(") 앞에 역 슬레쉬(\)를 추가해 주어 String 변수에 입력 시 묶어주는 큰따옴표(")와 JSON 형식 데이터 내의 큰따옴표와 직접적인 연결이 되지 않도록 하여 큰따옴표를 문자로서 저장되도록 하는 것 같다.

 

역 슬레쉬 - 위키백과

역슬래시(영어: backslash) 또는 역사선(逆斜線, 영어: reverse solidus)은 문장 부호의 일종이며, 1960년에 밥 베머가 ASCII 문자 집합을 만들면서 추가하였다. 

역슬래시(\)는 슬래시(/)를 좌우로 뒤집은 형태이며, 왼쪽 위에서 오른쪽 아래로 그은 선 모양이다. 

유닉스 계열 운영체제와 C, 펄과 같은 관련된 프로그래밍 언어에서, 역슬래시는 그 뒤에 따라오는 문자가 특수하게 처리되어야 한다는 것을 나타내며 종종 탈출 문자라고도 불린다. 예를 들어 여러 언어에서 "\n"은 개행 문자를 나타낸다. 또한 줄 끝에 오는 역슬래시는 그 줄과 다음 줄이 하나로 합쳐져야 한다는 것을 가리키기도 한다. 

도스와 마이크로소프트 윈도 시스템에서 역슬래시는 경로명에서 디렉터리 이름과 파일 이름을 구분하는 데 사용된다. 반면 유닉스 계열의 운영체제들은 그 목적으로 슬래시를 쓰기 때문에 사용자에게 종종 혼란을 주곤 한다. 역슬래시가 구분자로 쓰인 것은 디렉터리 개념이 없던 초기 운영체제들에서 슬래시를 명령줄 옵션을 나타내는 데 사용했기 때문이다. (유닉스에서는 하이픈을 대신 사용했다.) 그러나 이들 시스템에서도 보통 명령줄 옵션과 혼동이 되지 않는다면 슬래시를 역슬래시 대신에 쓸 수 있다.

 

상기 내용을 살펴보면 각 프로그램 언어마다 역 슬레쉬의 역할이 다름을 알 수 있다. 

참고로 역 슬레쉬(\)는 ISCII Code표에서 92(십진수)이다. 

 

 

JSON 형식의 데이터를 String 변수에 저장하기 위해 큰 따옴표(") 앞에 역 슬레쉬(\)를 입력해 주었다.  

String line = "{\"coord\":{\"lon\":126.98,\"lat\":37.57},\"weather\":[{\"id\":701,\"main\":\"Mist\",\"description\":\"mist\",\"icon\":\"50n\"}],\"base\":\"stations\",\"main\":{\"temp\":283.7,\"pressure\":1020,\"humidity\":100,\"temp_min\":281.15,\"temp_max\":286.15},\"visibility\":1200,\"wind\":{\"speed\":0.5,\"deg\":40},\"clouds\":{\"all\":75},\"dt\":1572715825,\"sys\":{\"type\":1,\"id\":5501,\"country\":\"KR\",\"sunrise\":1572731897,\"sunset\":1572769974},\"timezone\":32400,\"id\":1835848,\"name\":\"Seoul\",\"cod\":200}";

 

상기 변수를 한 줄로 사용하면 너무 길어 큰 따옴표를 사용하여 정리해 주었다. 

String line = "{\"coord\":{\"lon\":126.98,\"lat\":37.57}"
              ",\"weather\":[{\"id\":701,\"main\":\"Mist\",\"description\":\"mist\",\"icon\":\"50n\"}]"
              ",\"base\":\"stations\",\"main\":"
              "{\"temp\":283.7,\"pressure\":1020,\"humidity\":100,\"temp_min\":281.15,\"temp_max\":286.15}"
              ",\"visibility\":1200,\"wind\":{\"speed\":0.5,\"deg\":40}"
              ",\"clouds\":{\"all\":75},\"dt\":1572715825,\"sys\":"
              "{\"type\":1,\"id\":5501,\"country\":\"KR\",\"sunrise\":1572731897,\"sunset\":1572769974}"
              ",\"timezone\":32400,\"id\":1835848,\"name\":\"Seoul\",\"cod\":200}";

 

매개 변수와 리턴 값이 있는 사용자 함수를 만들어서 추출해 보겠다. 함수 형식은 아래와 같다.

"리턴 값 자료형" 함수명(매개변수) { 
  리턴 값 변수; 
  return 리턴 값 변수; 
}

 

원하는 값을 추출하기 위해 아래 스트링 클래스 함수를 사용하겠다. 

string.indexOf(string1);                                   // 스트링에서 스트링 1 문자열을 앞에서부터 찾아 첫 문자 인덱스 값 반환, 
                                                                                      못 찾을 경우 -1 반환 
string.indexOf(string1, from);                       // 시작 위치를 지정하여 검색 
string.substring(start index, end index);  // 스트링에서 새로운 스트링을 반환  
string.toInt();                                                      // 타당한 스트링(숫자)을 정수로 변환,  -값도 변환  
                                                                                     "1234" -> 1234, "-1234" -> -1234, "123abc" -> 123, "abc123" -> 0  
string.toFloat();                                                 // 타당한 스트링을 float으로 변환 
string.charAt(index);                                       //  스트링의 특정 문자 액세스 = String[index];

스트링 클래스 함수에 대해 더 알고 싶다면 이전 글 아두이노 - 시리얼통신 주요함수와 예제, String class를 참조하기 바란다.    

 

원하는 값을 추출하기 위해 다음과 같은 절차를 밟아서 값의 시작과 끝의 인덱스를 찾아가야 한다. 

 

1. 추출하고자 하는 변수의 시작 인덱스를 찾는다.   

2. 변수의 시작 인덱스 다음에 반드시 존재하는 ':'문자 인덱스를 찾는다.

3. ':'문자 인덱스에 1을 더한 인덱스의 문자가 큰 따옴표(")인지를 확인한다. 

- 스트링 값 추출

4. 만약 큰 따옴표(")이면 변수의 값은 스트링이므로 변숫값 마지막에는 반드시 큰 따옴표(")가 있다.

   ':'문자 인덱스에 2를 더한 문자부터 큰 따옴표(")를 검색하고 그 인덱스 값을 저장한다. 

   만약 큰 따옴표(")가 아니면 int 및 float 추출로 이동한다. 

5. ':'문자 인덱스에 2를 더한 문자부터 앞서 저장한 큰 따옴표(")의 인덱스까지의 스트링을

   string.substring(start index, end index) 함수를 사용하여 저장한다.

- int 및 float 추출

4. 만약 큰 따옴표(")가 아니면 변수의 값은 int 또는 float 값이고 변숫값의 마지막에는 반드시 콤마(,)가 있다.

   ':'문자 인덱스에 1을 더한 문자부터 콤마(,)를 검색하고 그 인덱스 값을 저장한다. 

5. ':'문자 인덱스에 1를 더한 문자부터 앞서 저장한 큰 따옴표(")의 인덱스까지의 스트링을

   string.substring(start index, end index) 함수를 사용하여 저장한다.                   

 

상기 절차대로 String 값을 추출하기 위하여 아래와 같이 사용자 함수를 만들어 주었다. 

String json_parser(String s, String a) {    // s :  JSON 형식 데이터, a : 추출하고자 하는 변수 이름      
  String val;
  if (s.indexOf(a) != -1) {                                                   // JSON 형식 데이터 s에서 변수 이름 a가 있으면
    int st_index = s.indexOf(a);                                        // 변수 이름의 첫 번째 단어 인덱스를 시작 인덱스로 저장
    int val_index = s.indexOf(':', st_index);                  // 시작 인덱스에서부터 검색해서 ':' 문자를 찾아 문자의 인덱스 저장
    if (s.charAt(val_index + 1) == '"') {                             // ':' 문자 다음 문자가 큰 따옴표(") 이면
      int ed_index = s.indexOf('"', val_index + 2);       // 큰 따옴표(") 다음 문자부터 다시 큰 따옴표(")를 검색, 인덱스 저장
      val = s.substring(val_index + 2, ed_index);         // 저장된 인덱스 값을 이용하여 스트링 추출
    }
    else {                                                                                   // ':' 문자 다음 문자가 큰 따옴표(")가 아니면 소수 또는 정수
      int ed_index = s.indexOf(',', val_index + 1);      // ':'문자 다음 문자부터 다시 콤마(,)를 검색, 인덱스 저장
      val = s.substring(val_index + 1, ed_index);         // 저장된 인덱스 값을 이용하여 스트링 추출
    }
  } 
  else {
    Serial.print(a); Serial.println(F(" is not available"));   // JSON 형식 데이터 s에서 변수 이름 a가 없으면 변수명 오류
  }
  return val;
}

 

사용자 함수의 사용방법은 아래와 같다.   

String description = json_parser(line, "description"); // 스트링 변수 line에서 description의 스트링 값 추출

 

아래 스케치는 상기 코드를 적용하여 JSON 형식 날씨 정보에서 원하는 값을 추출해내는 예제이다.

json_parsing.ino
0.00MB
String line = "{\"coord\":{\"lon\":126.98,\"lat\":37.57}"
              ",\"weather\":[{\"id\":701,\"main\":\"Mist\",\"description\":\"mist\",\"icon\":\"50n\"}]"
              ",\"base\":\"stations\",\"main\":"
              "{\"temp\":283.7,\"pressure\":1020,\"humidity\":100,\"temp_min\":281.15,\"temp_max\":286.15}"
              ",\"visibility\":1200,\"wind\":{\"speed\":0.5,\"deg\":40}"
              ",\"clouds\":{\"all\":75},\"dt\":1572715825,\"sys\":"
              "{\"type\":1,\"id\":5501,\"country\":\"KR\",\"sunrise\":1572731897,\"sunset\":1572769974}"
              ",\"timezone\":32400,\"id\":1835848,\"name\":\"Seoul\",\"cod\":200}";

void setup() {
  Serial.begin(9600);
  Serial.println(line);
  String description = json_parser(line, "description");
  Serial.println(description);
  String pressure = json_parser(line, "pressure");
  Serial.println(pressure);
  String wind = json_parser(line, "speed");
  Serial.println(wind);
  String Temp = json_parser(line, "temp");
  Serial.println(Temp);
}

void loop() {

}

String json_parser(String s, String a) { 
  String val;
  if (s.indexOf(a) != -1) {
    int st_index = s.indexOf(a);
    int val_index = s.indexOf(':', st_index);
    if (s.charAt(val_index + 1) == '"') {
      int ed_index = s.indexOf('"', val_index + 2);
      val = s.substring(val_index + 2, ed_index);
    }
    else {
      int ed_index = s.indexOf(',', val_index + 1);
      val = s.substring(val_index + 1, ed_index);
    }
  } 
  else {
    Serial.print(a); Serial.println(F(" is not available"));
  }
  return val;
}

 

이전 글의 아두이노 오픈 웨더 맵 API 날씨정보 수신 스케치에서 상기 코드를 이용하여 데이터를 추출해 보자. 

 

 

우선 오픈 웨더 맵 API에서 날씨 정보를 수신하게되면 스트링 변수 line에 아래와 같은 데이터가 들어오게 된다. 

HTTP/1.1 200 OK
Server: openresty
Date: Tue, 05 Nov 2019 11:38:02 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 443
Connection: close
X-Cache-Key: /data/2.5/weather?APPID=66e47f22f3570e6fd151ab5801a6cda7&q=seoul,kr
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST

{"coord":{"lon":126.98,"lat":37.57},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"base":"stations","main":{"temp":283.15,"pressure":1019,"humidity":87,"temp_min":281.15,"temp_max":285.15},"visibility":10000,"wind":{"speed":0.5,"deg":150},"clouds":{"all":1},"dt":1572953704,"sys":{"type":1,"id":5501,"country":"KR","sunrise":1572904824,"sunset":1572942652},"timezone":32400,"id":1835848,"name":"Seoul","cod":200}

 

앞선 JSON 테스트 스트링 변수는 상기 데이터의 { 날씨 정보 }의 데이터에 큰 따옴표(") 앞에 역 슬레쉬(\)를 붙여주어 테스트 하였었다. 이때에는 매개변수를 갖는 사용자 함수에 매개 변수로써 스트링 변수 line이 문제없이 입력되고 처리 되었으나 실제 와이파이를 통해 수신한 데이터를 매개 변수로써 사용하게 되면 정상적으로 parsing이 되지 않는다. 

 

원인을 살펴보니 스트링 변수 내에 문자'{' 있으면 매개 변수로써 사용될 때 사용자 함수 내부로 스트링 값이 전달되지 못하는 것을 찾을수 있었다. 따라서 스트링 변수 line에서 문자 '{'를 모두 제거해 주었고, 제거후에 스트링 변수가 정상적으로 사용자 함수에 전달되고 원하는 값을 추출할 수 있었다.   

 

해결 방법으로, 스트링 변수 line에 client.read() 값를 저장할 때 불리언 플래그를 이용하여 '{' 문자를 처음 만나면 변수 line에 저장하기 시작하고 '{'문자는 line에 저장하지 않도록 코드를 수정해 주거나 앞선 예제처럼 '{' 문자가 있어도 매개변수로써 잘 전달이 되었던 그대로 큰따옴표 앞에 역슬레쉬를 추가해 주면 된다. 

 

1. '{' 문자 제거 

bool receive_data = false;

while(client.available()) { // 날씨 정보가 들어오는 동안
  char c = client.read();
  if(c == '{') receive_data = true; // '{' 문자 있으면 데이터 저장 시작, else if 때문에 '{'는 저장 안함
  else if (receive_data == true) line += c;
}

 

2. 큰 따옴표(") 앞에 역 슬레쉬(\) 추가

bool receive_data = false; 

while(client.available()) { // 날씨 정보가 들어오는 동안
  char c = client.read();
  if(c == '{') receive_data = true; // '{' 문자 있으면 데이터 저장 시작
  if (receive_data == true) {
    if (c == '"') { // c가 큰따옴표(") 이면
      line += "\""; //  line에 역슬레쉬 추가(\") 입력 
    }
    else line += c;
  }
}

 

아래 스케치는 1번 '{' 문자 제거 방식을 사용하였다. 

open_weather_API_JSON_parsing.ino
0.00MB
#include "WiFiEsp.h"

#include <SoftwareSerial.h> 
#define rxPin 3 
#define txPin 2 
SoftwareSerial esp01(txPin, rxPin); // SoftwareSerial NAME(TX, RX);

char ssid[] = "SK_WiFiGIGA40F7";    // your network SSID (name)
char pass[] = "1712037218";         // your network password
int status = WL_IDLE_STATUS;        // the Wifi radio's status

// https://api.openweathermap.org/data/2.5/weather?q=Seoul,KR&APPID=YOUR_API_KEY
const char* host = "api.openweathermap.org";
const String url = "/data/2.5/weather?q=Seoul,KR&APPID=YOUR_API_KEY";

WiFiEspClient client; // WiFiEspClient 객체 선언

// RSS 날씨 정보 저장 변수
String line = "";
bool weather_check = false;

// RSS 날씨 정보 수신에 소요되는 시간 확인용 변수
unsigned long int count_time = 0;
bool count_start = false;
int count_val = 0;

void get_weather() {
  Serial.println(F("Starting connection to server..."));
  if (client.connect(host, 80)) {
    Serial.println(F("Connected to server"));
    client.print("GET " + url + " HTTP/1.1\r\n" +  
                 "Host: " + host + "\r\n" +  
                 "Connection: close\r\n\r\n");
    weather_check = true;
    count_start = true;
  }
}

void setup() {
  Serial.begin(9600);  //시리얼모니터
  esp01.begin(9600);   //와이파이 시리얼
  WiFi.init(&esp01);   // initialize ESP module
  while ( status != WL_CONNECTED) {   // attempt to connect to WiFi network
    Serial.print(F("Attempting to connect to WPA SSID: "));
    Serial.println(ssid);
    status = WiFi.begin(ssid, pass);    // Connect to WPA/WPA2 network
  }
  Serial.println(F("You're connected to the network"));
  Serial.println();
  delay(1000);
  get_weather();
}

void check_client_available() {  // loop()함수 진입 및 스캔 테스트 코드
  if (count_start == true) {
    if (millis() - count_time >= 100) {
      count_time = millis();
      count_val++;
      Serial.print(".");
      if (count_val == 30) count_start = false;
    }
  }
}

bool receive_data = false;

void loop() {
  check_client_available();
  while(client.available()) { // 날씨 정보가 들어오는 동안
    char c = client.read();
    if(c == '{') receive_data = true; // { 데이터 저장 시작, '{'는 저장 안함
    else if (receive_data == true) line += c;
  }
  if (weather_check == true) {
    if (!client.connected()) {   // 날씨정보 수신 종료됐으면
      receive_data = false;
      Serial.println();
      Serial.println(F("Disconnecting from server..."));
      client.stop();
      Serial.println(line);
      Serial.println(F("weather data for parsing"));
      String description = json_parser(line, "description");
      Serial.print(F("description: ")); Serial.println(description);
      String pressure = json_parser(line, "pressure");
      Serial.print(F("pressure: ")); Serial.println(pressure);
      String wind = json_parser(line, "speed");
      Serial.print(F("wind: ")); Serial.println(wind);
      String Temp = json_parser(line, "temp");
      Serial.print(F("Temp: ")); Serial.println(Temp);
      line = "";
      weather_check = false;
    }
  }
} 

String json_parser(String s, String a) { 
  String val;
  if (s.indexOf(a) != -1) {
    int st_index = s.indexOf(a);
    int val_index = s.indexOf(':', st_index);
    if (s.charAt(val_index + 1) == '"') {
      int ed_index = s.indexOf('"', val_index + 2);
      val = s.substring(val_index + 2, ed_index);
    }
    else {
      int ed_index = s.indexOf(',', val_index + 1);
      val = s.substring(val_index + 1, ed_index);
    }
  } 
  else {
    Serial.print(a); Serial.println(F(" is not available"));
  }
  return val;
}

 

관련 글

[arduino] - ESP8266 / ESP32 - HTTPClient.h 라이브러리 이용 날씨 정보 받기, RSS / API

[arduino] - 아두이노 - ESP01 모듈, 기상청 RSS / 오픈웨더맵 API 날씨 정보 받기

 

+ Recent posts