15주차_JSON 인코딩과 디코딩 - sookite22/SmartDivice_24 GitHub Wiki

목차

  • JSON
    • 직렬화
    • 역직렬화
  • 실습
    • JSON 인코딩(Encoding)
    • JSON 디코딩(Decoding)
  • 후기

1. JSON(JavaScript Object Notation)이란?

데이터를 저장하고 전송하는 데 사용되는 경량 데이터 교환 형식 이다. 주로 웹 애플리케이션에서 서버와 클라이언트 간의 데이터 교환에 사용된다. 자바나 파이썬, 자바스크립트 등 여러 언어에서 데이터 교환 형식으로 사용된다. XML 등 다른 데이터 포맷에 비해 간결하며, 손쉽게 자바스크립트 객체로 변환할 수 있어, 웹 어플리케이션에서 Server-Client간의 데이터 교환에 주로 JSON을 사용한다. 

JavaScript 객체 문법: 키(key) + 값(value)

ex) {"name" : "kim", "age" : 26}

주요 특징

  • 텍스트 기반: 사람이 읽고 쓸 수 있고, 기계가 쉽게 구문 분석하고 생성할 수 있다.
  • 언어 독립적: 많은 프로그래밍 언어에서 JSON을 사용하여 데이터를 쉽게 파싱하고 생성할 수 있다.
  • 구조적: 키-값 쌍으로 데이터를 구성한다.

1.1. 직렬화(Serialization)란?

외부 시스템에서 사용할 수 있도록 바이트 형태로 데이터를 변환하는 기술로, 쉽게 말해 사용하는 언어의 코드 내의 객체나, 해시 테이블, 딕셔너리 등을 JSON 파일로 만드는 과정이다.

객체(Object)문자열(string)

1.2. 역직렬화(Deserialization)란?

JSON 파일을 외부 시스템의 바이트 형태의 데이터(JSON)을 객체나 해시 테이블, 딕셔너리 등으로 변환하는 것이다.

문자열(string)자바스크립트 객체(Object)

2. 실습

네트워크 전송 시 사용하는 JSON 데이터의 길이가 길어지거나, 사용할 키 값의 종류가 많아지면 코드가 복잡해지고 가독성이 떨어진다. 이런 경우 ArduinoJson 라이브러리를 활용하면 좋다. 14주차 실습 코드 중, Access token을 요청하는 코드를 Json Library를 활용하도록 수정해 보자.

2.1. JSON 디코딩(Decoding)

"Arduinojson by Benoit Blanchon" 설치

아마 이전 실습에서 이미 설치를 했을 것이다.

스크린샷 2024-06-19 121619

버퍼 사이즈 계산 Step1:

https://arduinojson.org/v6/assistant/#/ 이동하여 다음과 같이 Processor와 Mode, Input type 설정을 진행한다.

스크린샷 2024-06-19 122044

버퍼 사이즈 계산 Step2:

샘플 JSON 데이터를 입력한다.

스크린샷 2024-06-19 123036

버퍼 사이즈 계산 Step3:

권장 버퍼 크기를 확인한다.

스크린샷 2024-06-19 123129

코드 작성

#include <ArduinoJson.h>
void setup() {
  // Initialize serial port
  Serial.begin(115200);
  while (!Serial) continue;
  delay(3000);
  // JSON 문서 할당
  //
  // 괄호 안의 200은 메모리 풀의 용량(바이트)입니다.
  // JSON 문서와 일치하도록 이 값을 변경하는 것을 잊지 마십시오.
  // https://arduinojson.org/v6/assistant 를 사용하여 용량을 계산합니다.
  StaticJsonDocument<256> doc;
  // StaticJsonDocument<N>은 스택에 메모리를 할당합니다.
  // 힙에 할당하는 DynamicJsonDocument로 대체할 수 있습니다.
  // DynamicJsonDocument doc(200);
  // JSON 입력 문자열.
  char json[] = R"rawliteral({
    "token_type":"bearer",
    "access_token":"c281d73b097",
    "expires_in":43199,
    "refresh_token":"0a0c90af08f",
    "refresh_token_expires_in":5184000,
    "scope":"account_email profile"
  })rawliteral";
  Serial.println(json);
  // Deserialize the JSON document
  DeserializationError error = deserializeJson(doc, json); // JSON 문자열을 doc 객체로 역직렬화
  // Test if parsing succeeds.
  if (error) {
    Serial.print(F("deserializeJson() failed: "));
    Serial.println(error.f_str());
    return;
  }
  // Fetch values.
  const char* access_token = doc["access_token"];
  const char* refresh_token = doc["refresh_token"];
  long expires_in = doc["expires_in"];
  // Print values.
  Serial.print("Access token : ");
  Serial.println(access_token);
  Serial.print("Refresh token : ");
  Serial.println(refresh_token);
  Serial.print("Expire time : ");
  Serial.println(expires_in);
}
void loop() {
  // not used in this example
}

이는 ArdoinoJson 라이브러리를 사용하여 JSON 문자열을 파싱하고, 특정 값을 추출한 후, 시리얼 모니터에 출력하는 프로그램이다.

결과 확인

스크린샷 2024-06-19 124526

JSON 문자열의 데이터를 파싱하여 각각의 값으로 출력된 것을 확인할 수 있다.

2.2. JSON 인코딩(Encoding)

https://arduinojson.org/v6/assistant/#/ 에서 필요한 버퍼 크기를 계산한다.

스크린샷 2024-06-19 122044

코드 작성

#include <ArduinoJson.h>

void setup() {
  // Initialize Serial port
  Serial.begin(115200);
  while (!Serial) continue;

  delay(3000);

   // JSON 문서 할당
   //
   // 괄호 안의 200은 이 문서에 할당된 RAM입니다.
   // 요구 사항에 맞게 이 값을 변경하는 것을 잊지 마십시오.
   // https://arduinojson.org/v6/assistant를 사용하여 용량을 계산합니다.
  StaticJsonDocument<256> doc;

   // StaticJsonObject는 스택에 메모리를 할당하고,
   // 힙에 할당하는 DynamicJsonDocument로 대체할 수 있습니다.
   //
   // DynamicJsonDocument doc(200);

   // 문서에 값 추가
  doc["object_type"] = "text";
  doc["text"] = "텍스트 영역입니다. 최대 " + String(200)+ "자 표시 가능합니다.";

  doc["link"]["web_url"]  = "https://developers.kakao.com";
  doc["link"]["mobile_web_url"] = "https://developers.kakao.com";

  doc["button_title"] = "바로 확인";

  String jsonString ;
  String jsonPretyString ;

  // 최소화된 JSON을 생성하여 직렬 포트로 보냅니다.
  //
  serializeJson(doc, jsonString);
  Serial.println(jsonString);
  // The above line prints:
  /*
   { "object_type": "text", "text": "텍스트 영역입니다. 최대 200자 표시 가능합니다.", "link": { "web_url": "https://developers.kakao.com", "mobile_web_url": "https://developers.kakao.com" }, "button_title": "바로 확인" }
  */

  // Start a new line
  Serial.println();

  // prettified JSON을 생성하고 직렬 포트로 보냅니다.
  //
  serializeJsonPretty(doc, jsonPretyString);
  Serial.println(jsonPretyString);
  /*
   {
        "object_type": "text",
        "text": "텍스트 영역입니다. 최대 200자 표시 가능합니다.",
        "link": {
            "web_url": "https://developers.kakao.com",
            "mobile_web_url": "https://developers.kakao.com"
        },
        "button_title": "바로 확인"
    }
  */
}

void loop() {
  // not used in this example
}
  • ArduinoJson 라이브러리를 사용하여 JSON 객체를 구성하고, 이를 직렬화하여 시리얼모니터에 출력한다.

결과 확인

스크린샷 2024-06-19 131945

주어진 data를 JSON 문자열로 출력한다. 줄바꿈 없는 양식이나 줄바꿈 있는 양식을 선택하여 출력할 수 있다.

2.3. 14주차 실습 코드 수정

#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

const char* ssid = "Your_SSID";          // 사용하는 WiFi 네트워크 이름 (SSID)
const char* password = "Your_Password";  // 사용하는 WiFi 네트워크 비밀번호

const String rest_api_key = "REST API KEY";
String access_token = "Access token";
String refresh_token = "Refresh token";

#define MsgSendInterval 3600  // 60 * 60 초, 즉 한시간 간격으로 전송
long timeout = 3600;          //시간을 초로 나타냄
int sensorValue = 0;
int sensorPin = 34;

void setup() {
  Serial.begin(115200);

  WiFi.begin(ssid, password);
  Serial.println("Connecting");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(1000);
  }
  Serial.print("\nConnected to WiFi : ");
  Serial.println(WiFi.localIP());
}
void loop() {
  if (timeout++ > MsgSendInterval)  // 1시간(60 * 60)에 1번씩 전송
  {
    if (isAccessTokenExpired() == true) {
      if (update_access_token() == false) {
        Serial.println("Access token update failed");
      }
    }
    sensorValue = analogRead(sensorPin);
    send_message();
    timeout = 0;
  }

  delay(1000);
}

// str문자열에서 start_string와 end_string사이의 문자열을 추출하는 함수
/*
String extract_string(String str, String start_string, String end_string)
{
  int index1 = str.indexOf(start_string) + start_string.length();
  int index2 = str.indexOf(end_string, index1);
  String value = str.substring(index1, index2);
  return value;
}
*/

bool isAccessTokenExpired() {
  HTTPClient http;
  bool returnVal = true;

  StaticJsonDocument<100> doc;
  /*
curl -v -X GET "https://kapi.kakao.com/v1/user/access_token_info" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}"
*/
  if (!http.begin("https://kapi.kakao.com/v1/user/access_token_info")) {
    Serial.println("\nfailed to begin http\n");
  }
  http.addHeader("Authorization", "Bearer " + access_token);

  int httpCode = http.GET();

  // httpCode will be negative on error
  if (httpCode > 0) {
    // file found at server
    if (httpCode == HTTP_CODE_OK) {
      String payload = http.getString();
      Serial.println(payload);
      // Deserialize the JSON document
      DeserializationError error = deserializeJson(doc, payload);
      long expire_time = doc["expires_in"];
      Serial.println(expire_time);
      if (expire_time > 0) {
        returnVal = false;
      } else {
        returnVal = true;
      }
    }
  } else {
    Serial.printf("[HTTP] GET... failed, error: %s\n",
                  http.errorToString(httpCode).c_str());
  }
  http.end();
  return returnVal;
}

void send_message() {
  HTTPClient http;

  StaticJsonDocument<300> doc;

  String url = "https://kapi.kakao.com/v2/api/talk/memo/default/send";
  if (!http.begin(url)) {
    Serial.println("\nfailed to begin http\n");
  }

  http.addHeader("Authorization", "Bearer " + access_token);
  http.addHeader("Content-Type", "application/x-www-form-urlencoded");

  int http_code;
  /* 카카오톡 default 템플릿
   template_object={
        "object_type": "text",
        "text": "텍스트 영역입니다. 최대 200자 표시 가능합니다.",
        "link": {
            "web_url": "https://developers.kakao.com",
            "mobile_web_url": "https://developers.kakao.com"
        },
        "button_title": "바로 확인"
    }
    */
  doc["object_type"] = "text";
  doc["text"] = "토양 센서 값 :" + String(sensorValue);
  //doc["link"]["web_url"] = "https://www.naver.com";
  JsonObject obj = doc.createNestedObject();
  doc["link"] = obj;

  String data;
  // Generate the minified JSON and send it to the Serial port.
  //
  serializeJson(doc, data);
  Serial.println(data);

  http_code = http.POST("template_object=" + data);
  Serial.print("HTTP Response code: ");
  Serial.println(http_code);

  String response;
  if (http_code > 0) {
    response = http.getString();
    Serial.println(response);
  }

  http.end();
}

/*
curl -v -X POST "https://kauth.kakao.com/oauth/token" \
 -H "Content-Type: application/x-www-form-urlencoded" \
 -d "grant_type=refresh_token" \
 -d "client_id=${REST_API_KEY}" \
 -d "refresh_token=${USER_REFRESH_TOKEN}"
*/
bool update_access_token() {
  HTTPClient http;
  bool retVal = false;
  StaticJsonDocument<400> doc;

  String url = "https://kauth.kakao.com/oauth/token";
  String nrefresh_token = "";
  if (!http.begin(url)) {
    Serial.println("\nfailed to begin http\n");
  }
  http.addHeader("Content-Type", "application/x-www-form-urlencoded");
  int http_code;
  String data = "grant_type=refresh_token&client_id=" + rest_api_key + "&refresh_token=" + refresh_token;
  Serial.println(data);
  http_code = http.POST(data);
  Serial.print("HTTP Response code: ");
  Serial.println(http_code);

  String response;
  if (http_code > 0) {
    response = http.getString();
    Serial.println(response);

    // Deserialize the JSON document
    DeserializationError error = deserializeJson(doc, response);

    const char* atoken = doc["access_token"];
    const char* rtoken = doc["refresh_token"];

    Serial.print("Access token : ");
    Serial.println(atoken);
    Serial.print("New refresh token : ");
    Serial.println(rtoken);

    access_token = atoken;
    //만료 1개월전부터 갱신되므로 data가 없을 수도 있음
    if (rtoken != NULL) {
      refresh_token = rtoken;
    }

    retVal = true;
  } else {
    retVal = false;
  }
  http.end();
  return retVal;
}
  • Access token을 요청하는 코드를 Json Library를 활용하도록 수정한 코드이다.

결과 확인

KakaoTalk_20240619_140601944

  • 수정된 코드로도 14주차 실습 결과와 동일하게 토양 센서 값이 일정한 시간 간격으로 카카오톡 메세지로 전송되는 것을 확인할 수 있다.

3. 후기

JSON을 통해 인코딩과 디코딩을 적절히 적용해보고 이해할 수 있게 된 중요한 실습 내용 중 하나였던 것 같다. JSON이라는 것은 이론 수업에서만 배워본 적이 있는데, 이렇게 직접 실습을 통해 배우며 헷갈리거나 다소 이해가 부족했던 부분을 채울 수 있게 되었다.

⚠️ **GitHub.com Fallback** ⚠️