15주 차 JSON 인코딩과 디코딩 - park-02/2024-1_Smart-Devices GitHub Wiki

목차

  • JSON의 정의
  • JSON 인코딩
  • JSON 디코딩
  • JSON 인코딩 및 디코딩 주의사항
  • 실습[1]

JSON의 정의

JSON (JavaScript Object Notation)은 데이터를 저장하고 교환하는 데 사용되는 가볍고 인간이 읽을 수 있는 텍스트 기반 데이터 포맷입니다. JSON은 JavaScript에서 객체를 정의하는 구문에서 유래했지만, 대부분의 프로그래밍 언어에서 데이터를 직렬화하고 파싱하는 데 널리 사용됩니다.

JSON 인코딩

JSON 인코딩은 데이터를 JSON 형식으로 변환하는 과정입니다. 주로 객체나 배열을 JSON 문자열로 변환하여 데이터를 전송하거나 저장할 때 사용됩니다. 대부분의 프로그래밍 언어에서는 JSON 인코딩을 지원하며, 각 언어에 따라 방식이 조금씩 다를 수 있습니다.

import json

# Python 객체 정의
person = {
    "name": "John",
    "age": 30,
    "city": "New York"
}

# JSON 인코딩
json_string = json.dumps(person)

print(json_string)

위 예제에서는 Python의 json.dumps() 함수를 사용하여 person 객체를 JSON 문자열로 변환하고 출력합니다. 결과는 다음과 같습니다:

{"name": "John", "age": 30, "city": "New York"}

JSON 디코딩

JSON 디코딩은 JSON 형식의 데이터를 원래의 데이터 형식(일반적으로 객체나 배열)으로 변환하는 과정입니다. 이 과정은 주로 JSON 문자열을 받아서 프로그램에서 사용할 수 있는 데이터 구조로 변환할 때 사용됩니다.

import json

# JSON 문자열
json_string = '{"name": "John", "age": 30, "city": "New York"}'

# JSON 디코딩
person = json.loads(json_string)

print(person)
print(person["name"])  # John 출력

위 예제에서는 Python의 json.dumps() 함수를 사용하여 person 객체를 JSON 문자열로 변환하고 출력합니다. 결과는 다음과 같습니다:

{'name': 'John', 'age': 30, 'city': 'New York'}
John

JSON 인코딩 및 디코딩 주의사항

  • 데이터 유형: JSON은 문자열, 숫자, 배열, 객체, 불리언 등의 기본 데이터 유형을 지원합니다. 하지만 일부 언어에서는 날짜나 함수와 같은 특정 데이터 유형을 JSON으로 직접 변환할 때 추가적인 처리가 필요할 수 있습니다.

  • 오류 처리: JSON 디코딩 시 입력이 유효하지 않은 경우(예: 유효하지 않은 JSON 형식) 오류가 발생할 수 있습니다. 이를 처리하기 위해 대부분의 언어에서는 예외 처리를 사용하는 것이 좋습니다.

  • 언어별 차이: 각 언어의 JSON 라이브러리는 성능, 기능, 사용법 등에서 차이가 있을 수 있으므로 해당 언어의 공식 문서나 라이브러리 문서를 참조하는 것이 좋습니다.

JSON 인코딩과 디코딩은 데이터를 서로 다른 형식으로 변환하면서도 간편하게 데이터를 저장하고 전송할 수 있는 매우 유용한 기술입니다.

실습[1]

실습 내용

ArduinoJson 라이브러리는 Arduino에서 JSON 데이터를 처리하는 데 매우 유용한 라이브러리입니다. 특히 네트워크 통신 시에 JSON 데이터를 사용할 때, 데이터의 가독성을 높이고 코드를 간소화하는 데 도움이 됩니다.

여기서는 ArduinoJson 라이브러리를 사용하여 Access token을 요청하는 예제 코드를 작성해 보겠습니다. 이 코드는 JSON 형식으로 요청 데이터를 생성하고, 응답 데이터를 파싱하는 과정을 포함합니다.

asd

JSON 디코딩

123123

  • 버퍼 사이즈 계산 Step2: 샘플 Json 데이터 입력합니다.

123123

  • 버퍼 사이즈 계산 Step3: 권장 버퍼 크기 확인합니다.

123123

소스코드

#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);
  // 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
}

image

소스코드 설명

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

image

  • DeserializationError error = deserializeJson(doc, json);: JSON 문자열을 doc 객체로 역 직렬 화합니다. if (error) {...}: 역 직렬화에 실패한 경우, 에러 메시지를 시리얼 모니터에 출력하고 setup 함수를 종료합니다.

image

  • const char* access_token = doc["access_token"];: JSON 문서에서 access_token 값을 추출합니다. const char* refresh_token = doc["refresh_token"];: JSON 문서에서 refresh_token 값을 추출합니다. long expires_in = doc["expires_in"];: JSON 문서에서 expires_in 값을 추출합니다.

JSON 인코딩

123123

#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
}

image

소스코드 설명

  • 이 코드는 ArduinoJson 라이브러리를 사용하여 JSON 객체를 구성하고, 이를 직렬화하여 시리얼 모니터에 출력하는 예제입니다.

image

  • 크기가 256바이트인 정적 JSON 문서를 생성합니다.

image

  • JSON 객체 구성합니다.

image

  • 객체를 JSON 문자열로 직렬화하여 jsonString에 저장 -> 최소화된(minified) JSON 문자열을 시리얼 모니터에 출력합니다.

챕터 11의 코드 중, Access token을 요청하는 코드를 Json Library를 활용하도록 수정한 코드입니다.

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

const char* ssid = "..";          // 사용하는 WiFi 네트워크 이름 (SSID)
const char* password = "123456799";  // 사용하는 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;
}

실행 결과

image

챕터 11과 동일하게 토양 센서 값이 일전한 시간 간격으로 카카오톡 메시지로 전송되는 것을 확인할 수 있습니다.

후기

JSON에 대한 이론적인 이해만으로는 부족한 부분이 많았습니다. 하지만 직접 코드를 작성하고 테스트하며 JSON의 인코딩과 디코딩을 경험하다 보니 많은 것을 배웠습니다. 이런 경험은 개념을 실제로 응용하고 이해하는 데 큰 도움이 되었습니다. JSON은 데이터를 효율적으로 표현하고 전달하는 도구이기 때문에, 프로그래밍에서 매우 유용하게 사용됩니다.

이번 학기가 너무 빨리 지나갔습니다. 소프트웨어를 통해 ESP32라는 하드웨어를 제어하고 동작시키는 과정이 정말 흥미롭고 유익했습니다. 15주 동안의 경험을 통해 여름 방학 동안 더 많은 시간을 들여 ESP32를 공부하고 프로젝트에 도전해보려고 합니다.

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