week__15 (NodeRed 및 ArduinoJson) - JINEEH/SmartDevice_JinHee GitHub Wiki

NodeRed 및 ArduinoJson 활용

1. 학습목표

1-1. NodeRed 설치 및 활용
1-2. ArduinoJson 라이브러리 사용 및 토양 습도 센서 실습

2. NodeRed

2-1. 의미: 하드웨어 장치들, API, 온라인 서비스를 IoT의 일부로 와이어링시키기 위해 IBM이 개발한 시각 프로그래밍을 위한 플로 기반 개발 도구 
           - 노드를 기본으로 하며 각 노드를 선으로 연결하여 플로우를 만드는 방식
           - 노드는 특정 기능을 수행하는 프로그램 묶음 => 플로우
2-2. 특징: 프로토타입을 빠른 시간 내에 만들어서 기능/서비스를 구현 가능함
           - IoT 와이어링을 위해 데이터들은 가시화 과정 필요(GUI 기능)
           - 브라우저 기반 플로 편집기를 제공 및 자바스크립트 함수 개발 가능
           - 컨트롤러를 구현하는데 가장 적합한 개발 환경임
           - 기본 제공 외에 오픈소스로 제공되는 노드 => 노드의 확장성(2020년 기준 약 2,600여개 이상)
           - 로우코드(Low-code) 기반 개발 플랫폼 => 코드를 적게 작성하고도 응용 시스템을 빠르게 개발 가능
           - 개인이나 소규모 조직에서 IoT 기기 제어용으로 사용하는 경우에는 라즈베리 파이에 설치하는 것이 좋음 => 전력 사용량 및 비용을 고려했을 때 라즈베리 파이가 합리적
2-3. 작동환경: 대부분의 OS에서 작동 가능함
               - Node.js: 인터넷 브라우저인 크롬의 자바스크립트 런타임 엔진을 일반적인 컴퓨터에서 작동 가능
               - Node-RED는 Node.js 환경에서 작동함
               - Node.js 설치 후 Node-RED 설치 순서로 진행 
               - Node-RED 편집기와 Node-RED 대시보드는 인터넷 브라우저를 사용하여 접속 가능

3. Node-Red 활용

3-1. Node-RED 설치 방법
     - [Node-RED 설치](https://nodejs.org/en) 에 접속하여 윈도우용 Node-RED 다운로드
     - 다운로드 후 cmd 창에 'npm install -g --unsafe-perm node-red' 입력하여 설치
     - 설치 후 cmd 창에 'npm install node-red-dashboard' 입력하여 대시보드 설치 또는 Node-RED 접속 후 직접 설치 가능
     - cmd 창에 'node-red' 입력하여 실행
     - 주소창에 localhost:1880 입력하여 Node-RED 실행 가능
  • Node-RED에서 직접 대시보드 설치하는 방법
  • node-red-dashboard 검색하여 다운로드 15주차 nodered(1)

15주차 nodered(2)

  • Node-RED를 사용한 간단한 예제
  • inject와 debug 사용('안녕하세요!' 입력 후 debug 통해 출력 확인)

15주차 nodered(3)

4. Json(JavaScript Object Notation)

4-1. 의미: 데이터 전송 및 교환을 위한 표준화된 텍스트 형식
           - 전송하고자 하는 데이터를 속성과 값으로 나열하여 표현
4-2. 특징: 웹 브라우저와 웹 서버간의 데이터를 교환할 때 사용
           - XML에 비해 가볍고 용량을 적게 차지함
           - 자바스크립트 형식을 따름

5. ArduinoJson 라이브러리 설치 및 실습

5-1. Json 문자열의 데이터 파싱 후의 값 출력
5-2. Json 데이터의 길이가 길어지거나 사용할 키 값의 종류가 많아지면 코드가 복잡해지고 가독성이 떨어짐 
     => 해결방안: ArduinoJson 라이브러리를 사용하여 버퍼 사이즈 확인 및 불필요한 크기 사용 X
  • Arduino IDE에서 [Arduinojson] 설치

15주차 json

  • Arduino Json 에서 Processor, Mode, Input type 설정(JSON 디코딩)
  • Processor: ESP32
  • Mode: Deserialize
  • Input type: char[N]
  • Deserialize(역직렬화): 문자열을 자바스크립트 객체로 변환, 저수준의 형식을 읽을 수 있는 객체나 기타 데이터 구조로 변환

15주차 json(2)

  • 권장 버퍼 크기 확인
  • 권장 버퍼 사이즈 96으로 확인 가능함

15주차 json크기할당(1)

  • 실행코드(json(1) 코드 - 크기 할당 96으로 변경해야 업로드 가능)
#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<96> doc; //크기가 96바이트인 JSON 문서 생성
  // 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
}
  • 실습 결과
  • 시리얼 모니터를 통해 출력 값 확인

15주차 json(3)

  • Arduino Json 에서 Processor, Mode, Input type 설정(JSON 인코딩)
  • Processor: ESP32
  • Mode: Serialize
  • Input type: char[N]
  • Serialize(직렬화): 컴퓨터 메모리 상에 존재하는 객체를 문자열로 변환, 객체나 데이터 구조를 네트워크나 저장소를 통한 전송에 적합한 형식으로 변환

15주차 json(4)

  • 권장 버퍼 크기 확인
  • 권장 버퍼 사이즈 192로 확인 가능함

15주차 json크기할당(2)

  • 실행코드(json(2) 코드 - 크기 할당 192로 변경해야 업로드 가능)
#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<192> 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을 생성하여 직렬 포트로 보냅니다.
  // 객체를 JSON 문자열로 직렬화하여 jsonString에 저장
  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
}
  • 실습 결과
  • 최소화된 JSON 문자열을 시리얼 모니터를 통해 출력 값 확인

15주차 json(5)

6. 토양 습도 센서 실습

6-1. JSON 사용하여 14주차 실습 수정
6-2. 실습 준비물: ESP32, ESP32 확장 실드, 토양 습도 센서, 점퍼 케이블
6-3. 센서 값 해석: 0 ~1500 건조한 토양 / 1500~2000 정상 / 2000이상 물 속
  • 실행코드
  • 시간 변경하여 30분(1800초) 간격으로 전송
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

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

const String rest_api_key = "";
String access_token = "";
String refresh_token = "";

#define MsgSendInterval 1800  // 30분 단위로 임의 변경
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)  // 
  {
    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;
}
  • 실습 결과
  • 첫번째 토양 센서 값이 1349로 건조한 토양에 속했으나, 토양에 물을 준 이후인 네번째 토양 센서 값(1786)으로 정상적인 토양 상태로 변화함

7. 부가설명

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