15주차_JSON 인코딩과 디코딩 - sookite22/SmartDivice_24 GitHub Wiki
- JSON
- 직렬화
- 역직렬화
- 실습
- JSON 인코딩(Encoding)
- JSON 디코딩(Decoding)
- 후기
데이터를 저장하고 전송하는 데 사용되는 경량 데이터 교환 형식 이다. 주로 웹 애플리케이션에서 서버와 클라이언트 간의 데이터 교환에 사용된다. 자바나 파이썬, 자바스크립트 등 여러 언어에서 데이터 교환 형식으로 사용된다. XML 등 다른 데이터 포맷에 비해 간결하며, 손쉽게 자바스크립트 객체로 변환할 수 있어, 웹 어플리케이션에서 Server-Client간의 데이터 교환에 주로 JSON을 사용한다.
JavaScript 객체 문법: 키(key) + 값(value)
ex) {"name" : "kim", "age" : 26}
- 텍스트 기반: 사람이 읽고 쓸 수 있고, 기계가 쉽게 구문 분석하고 생성할 수 있다.
- 언어 독립적: 많은 프로그래밍 언어에서 JSON을 사용하여 데이터를 쉽게 파싱하고 생성할 수 있다.
- 구조적: 키-값 쌍으로 데이터를 구성한다.
외부 시스템에서 사용할 수 있도록 바이트 형태로 데이터를 변환하는 기술로, 쉽게 말해 사용하는 언어의 코드 내의 객체나, 해시 테이블, 딕셔너리 등을 JSON 파일로 만드는 과정이다.
객체(Object) → 문자열(string)
JSON 파일을 외부 시스템의 바이트 형태의 데이터(JSON)을 객체나 해시 테이블, 딕셔너리 등으로 변환하는 것이다.
문자열(string) → 자바스크립트 객체(Object)
네트워크 전송 시 사용하는 JSON 데이터의 길이가 길어지거나, 사용할 키 값의 종류가 많아지면 코드가 복잡해지고 가독성이 떨어진다. 이런 경우 ArduinoJson 라이브러리를 활용하면 좋다. 14주차 실습 코드 중, Access token을 요청하는 코드를 Json Library를 활용하도록 수정해 보자.
아마 이전 실습에서 이미 설치를 했을 것이다.
https://arduinojson.org/v6/assistant/#/ 이동하여 다음과 같이 Processor와 Mode, Input type 설정을 진행한다.
샘플 JSON 데이터를 입력한다.
권장 버퍼 크기를 확인한다.
#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 문자열을 파싱하고, 특정 값을 추출한 후, 시리얼 모니터에 출력하는 프로그램이다.
JSON 문자열의 데이터를 파싱하여 각각의 값으로 출력된 것을 확인할 수 있다.
https://arduinojson.org/v6/assistant/#/ 에서 필요한 버퍼 크기를 계산한다.
#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 객체를 구성하고, 이를 직렬화하여 시리얼모니터에 출력한다.
주어진 data를 JSON 문자열로 출력한다. 줄바꿈 없는 양식이나 줄바꿈 있는 양식을 선택하여 출력할 수 있다.
#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를 활용하도록 수정한 코드이다.
- 수정된 코드로도 14주차 실습 결과와 동일하게 토양 센서 값이 일정한 시간 간격으로 카카오톡 메세지로 전송되는 것을 확인할 수 있다.
JSON을 통해 인코딩과 디코딩을 적절히 적용해보고 이해할 수 있게 된 중요한 실습 내용 중 하나였던 것 같다. JSON이라는 것은 이론 수업에서만 배워본 적이 있는데, 이렇게 직접 실습을 통해 배우며 헷갈리거나 다소 이해가 부족했던 부분을 채울 수 있게 되었다.