개발자/Arduino

Decoding and Encoding JSON with Arduino Nano 33 IoT 2

지구빵집 2020. 10. 9. 23:40
반응형

 

 

Arduino로 JSON 데이터 주고받기와 OpenWeatherMap API 두 번째 포스팅  

 

Arduino로 API 요청 프로그래밍하기 

 

이전 포스팅과 이어서 진행합니다. 이전 포스팅을 보시려면 아래 링크를 따라가서 확인하십시요.

 

Decoding and Encoding JSON with Arduino Nano 33 IoT 1

 

이제 지역 날씨 데이터를 반환하는 URL이 생겼습니다. 이 작업을 자동화하고 Arduino 또는 ESP8266 프로젝트에서 해당 데이터에 액세스 할 수 있습니다. 다음은 켈빈 및 습도로 온도를 반환하기 위해 이더넷 실드가있는 Arduino에 업로드해야하는 전체 스크립트입니다. 아래 코드는 Ethernet을 사용합니다. Nano 33 iot에서 제대로 출력하는 코드는 끝 부분에 있습니다.

 

/*
 * Rui Santos 
 * Complete Project Details http://randomnerdtutorials.com
 * Based on the Arduino Ethernet Web Client Example
 * and on the sketch "Sample Arduino Json Web Client" of the Arduino JSON library by Benoit Blanchon (bblanchon.github.io/ArduinoJson)
 */

#include <ArduinoJson.h>
#include <Ethernet.h>
#include <SPI.h>

EthernetClient client;

// Name address for Open Weather Map API
const char* server = "api.openweathermap.org";

// Replace with your unique URL resource
const char* resource = "REPLACE_WITH_YOUR_URL_RESOURCE";

// How your resource variable should look like, but with your own COUNTRY CODE, CITY and API KEY (that API KEY below is just an example):
//const char* resource = "/data/2.5/weather?q=Porto,pt&appid=bd939aa3d23ff33d3c8f5dd1";

const unsigned long HTTP_TIMEOUT = 10000;  // max respone time from server
const size_t MAX_CONTENT_SIZE = 512;       // max size of the HTTP response

byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};

// The type of data that we want to extract from the page
struct clientData {
  char temp[8];
  char humidity[8];
};

// ARDUINO entry point #1: runs once when you press reset or power the board
void setup() {
  Serial.begin(9600);
  while (!Serial) {
    ;  // wait for serial port to initialize
  }
  Serial.println("Serial ready");
  if(!Ethernet.begin(mac)) {
    Serial.println("Failed to configure Ethernet");
    return;
  }
  Serial.println("Ethernet ready");
  delay(1000);
}

// ARDUINO entry point #2: runs over and over again forever
void loop() {
  if(connect(server)) {
    if(sendRequest(server, resource) && skipResponseHeaders()) {
      clientData clientData;
      if(readReponseContent(&clientData)) {
        printclientData(&clientData);
      }
    }
  }
  disconnect();
  wait();
}

// Open connection to the HTTP server
bool connect(const char* hostName) {
  Serial.print("Connect to ");
  Serial.println(hostName);

  bool ok = client.connect(hostName, 80);

  Serial.println(ok ? "Connected" : "Connection Failed!");
  return ok;
}

// Send the HTTP GET request to the server
bool sendRequest(const char* host, const char* resource) {
  Serial.print("GET ");
  Serial.println(resource);

  client.print("GET ");
  client.print(resource);
  client.println(" HTTP/1.1");
  client.print("Host: ");
  client.println(host);
  client.println("Connection: close");
  client.println();

  return true;
}

// Skip HTTP headers so that we are at the beginning of the response's body
bool skipResponseHeaders() {
  // HTTP headers end with an empty line
  char endOfHeaders[] = "\r\n\r\n";

  client.setTimeout(HTTP_TIMEOUT);
  bool ok = client.find(endOfHeaders);

  if (!ok) {
    Serial.println("No response or invalid response!");
  }
  return ok;
}

// Parse the JSON from the input string and extract the interesting values
// Here is the JSON we need to parse
/*{
    "coord": {
        "lon": -8.61,
        "lat": 41.15
    },
    "weather": [
        {
            "id": 800,
            "main": "Clear",
            "description": "clear sky",
            "icon": "01d"
        }
    ],
    "base": "stations",
    "main": {
        "temp": 296.15,
        "pressure": 1020,
        "humidity": 69,
        "temp_min": 296.15,
        "temp_max": 296.15
    },
    "visibility": 10000,
    "wind": {
        "speed": 4.6,
        "deg": 320
    },
    "clouds": {
        "all": 0
    },
    "dt": 1499869800,
    "sys": {
        "type": 1,
        "id": 5959,
        "message": 0.0022,
        "country": "PT",
        "sunrise": 1499836380,
        "sunset": 1499890019
    },
    "id": 2735943,
    "name": "Porto",
    "cod": 200
}*/

bool readReponseContent(struct clientData* clientData) {
  // Compute optimal size of the JSON buffer according to what we need to parse.
  // See https://bblanchon.github.io/ArduinoJson/assistant/
  const size_t bufferSize = JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(1) + 
      2*JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(4) + JSON_OBJECT_SIZE(5) + 
      JSON_OBJECT_SIZE(6) + JSON_OBJECT_SIZE(12) + 390;
  DynamicJsonBuffer jsonBuffer(bufferSize);

  JsonObject& root = jsonBuffer.parseObject(client);

  if (!root.success()) {
    Serial.println("JSON parsing failed!");
    return false;
  }

  // Here were copy the strings we're interested in using to your struct data
  strcpy(clientData->temp, root["main"]["temp"]);
  strcpy(clientData->humidity, root["main"]["humidity"]);
  // It's not mandatory to make a copy, you could just use the pointers
  // Since, they are pointing inside the "content" buffer, so you need to make
  // sure it's still in memory when you read the string

  return true;
}

// Print the data extracted from the JSON
void printclientData(const struct clientData* clientData) {
  Serial.print("Temp = ");
  Serial.println(clientData->temp);
  Serial.print("Humidity = ");
  Serial.println(clientData->humidity);
}

// Close the connection with the HTTP server
void disconnect() {
  Serial.println("Disconnect");
  client.stop();
}

// Pause for a 1 minute
void wait() {
  Serial.println("Wait 60 seconds");
  delay(60000);
}

 

참고 : 리소스 변수를 고유한 OpenWeatherMap URL 리소스로 바꾸십시오. 즉 이전 포스티에서 브라우져에서 날씨 데이터를 받아오던 그 코드를 말합니다. 코드의 18라인, 다를 수 있으니까요. Nano 33 IoT 보드에 WiFi 면결만 하고 업로딩 하니 아래 메세지가 뜬다. 보니까 아래를 따라하지 았았다. 무언가 잘못된거죠. 사실 무언가 해보기 전에는 아무것도 알지 못합니다. 진리죠.

 

아래는 실패한 결과입니다. 

 

Serial ready
Attempting to connect to SSID: SK_WiFiGIGAA988
Connected to wifi
SSID: SK_WiFiGIGAA988
IP Address: 192.168.35.75
signal strength (RSSI):-72 dBm
Connect to api.openweathermap.org
Connected
GET REPLACE_WITH_YOUR_URL_RESOURCE
JSON parsing failed!
Disconnect
Wait 60 seconds

 

지금부터 잘 따라합니다.

 

const char* resource = "REPLACE_WITH_YOUR_URL_RESOURCE";

 

제 경우에 위 코드를 이렇게 수정합니다.

 

const char* resource = "/data/2.5/weather?q=seoul,KR&APPID=34b3527445f4bb5a2b8b-----------";

 

프로젝트 코드 수정 - 아래는 이미 적용되어 있네요. 너무 잘되서 의심스러운 코드를 스텝 다 뛰어 넘어 첨부하니 단계를 지나서 참고하십시요. WiFi 적용, Nano 33 IoT 보드입니다.

 

이 예에서 Arduino는 원하는 서비스 (이 경우 OpenWeatherMap API)에 대한 HTTP GET 요청을 수행하지만 다른 웹 서비스를 요청하도록 변경할 수 있습니다. Arduino 코드를 한 줄씩 설명하지 않겠습니다.

 

이 프로젝트에서는 JSON 응답을 디코딩 / 파싱하기 위해 Arduino 코드에서 변경해야하는 사항을 이해하는 것이 중요합니다. 다음 세 단계를 따르십시오.

 

1 단계 – 구조체

 

API에서 추출 할 정보를 저장할 수있는 데이터 구조를 만듭니다. 이 경우 온도와 습도를 문자 배열에 저장하려고합니다.

 

struct clientData {
  char temp[8];
  char humidity[8];
};

 

2 단계 – JsonBuffer 크기

 

ArduinoJson Assistant로 이동하여 전체 OpenWeatherMap API 응답을 입력 필드에 복사합니다. 아래그림의 입력에 웨더맵에 요청하여 받은 데이터를 넣으면 메모리 할당 구문이 생성됩니다.

 

 

제 경우에는 생성 된 표현식을 복사합니다 (위 그림 참조).

 

JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(1) + 2*JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(4) + JSON_OBJECT_SIZE(5) + JSON_OBJECT_SIZE(6) + JSON_OBJECT_SIZE(13)

 

API에서 JSON 응답을 디코딩하기 위해 적절한 메모리를 할당하려면 ArduinoJson Assistant에서 생성된 JsonBuffer 크기로 readReponseContent( ) 함수를 편집해야합니다.

 

bool readReponseContent(struct clientData* clientData) {
  const size_t bufferSize = JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(1) + 
    2*JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(4) + JSON_OBJECT_SIZE(5) + 
    JSON_OBJECT_SIZE(6) + JSON_OBJECT_SIZE(12) + 390; 

  DynamicJsonBuffer jsonBuffer(bufferSize);
  JsonObject& root = jsonBuffer.parseObject(client);

 

여전히 readReponseContent( ) 함수 내에서 프로젝트에 필요한 변수를 구조체 데이터에 복사해야 합니다.

 

strcpy(clientData->temp, root["main"]["temp"]);
strcpy(clientData->humidity, root["main"]["humidity"]);

 

단계 # 3 – 디코딩 된 데이터 액세스

 

그런 다음 Arduino 코드에서 디코딩 된 JSON 데이터에 쉽게 액세스하여 작업을 수행 할 수 있습니다. 이 예에서는 Arduino IDE 직렬 모니터의 온도와 습도를 켈빈 단위로 간단히 인쇄합니다.

 

void printclientData(const struct clientData* clientData) {
  Serial.print("Temp = ");
  Serial.println(clientData->temp);
  Serial.print("Humidity = ");
  Serial.println(clientData->humidity);
}

 

이 포스팅의 시작 부분에 넣은 코드와 달라진 것은 별로 없지만 첨부 합니다. 헤더 파일과 설정 부분, Setup 부분만 다릅니다. 참고하십시요.

 

#include <ArduinoJson.h>
#include <Ethernet.h>
#include <SPI.h>
#include <SPI.h>
#include <WiFiNINA.h>

//EthernetClient client;
WiFiClient client;

// Name address for Open Weather Map API
const char* server = "api.openweathermap.org";

// Replace with your unique URL resource
const char* resource = "/data/2.5/weather?q=seoul,KR&APPID=34b3527445f4bb5a2b8be7----------";

// How your resource variable should look like, but with your own COUNTRY CODE, CITY and API KEY (that API KEY below is just an example):
//const char* resource = "/data/2.5/weather?q=Porto,pt&appid=bd939aa3d23ff33d3c8f5dd1";

const unsigned long HTTP_TIMEOUT = 10000;  // max respone time from server
const size_t MAX_CONTENT_SIZE = 512;       // max size of the HTTP response


#include "arduino_secrets.h" 
///////please enter your sensitive data in the Secret tab/arduino_secrets.h
char ssid[] = SECRET_SSID;        // your network SSID (name)
char pass[] = SECRET_PASS;    // your network password (use for WPA, or use as key for WEP)
int keyIndex = 0;            // your network key Index number (needed only for WEP)

int status = WL_IDLE_STATUS;

// The type of data that we want to extract from the page
struct clientData {
  char temp[8];
  char humidity[8];
};

 

셋업 부분은 이렇게 바뀝니다. WiFi 연결하는 부분이죠.

 

void setup() {
  Serial.begin(9600);
  while (!Serial) {
    ;  // wait for serial port to initialize
  }
  
  Serial.println("Serial ready");
  while (status != WL_CONNECTED) {
    Serial.print("Attempting to connect to SSID: ");
    Serial.println(ssid);
    // Connect to WPA/WPA2 network. Change this line if using open or WEP network:
    status = WiFi.begin(ssid, pass);

    // wait 10 seconds for connection:
    delay(10000);
  }
  Serial.println("Connected to wifi");
  printWifiStatus();
}

 

프로그램에 없는 printwifistatus( ) 코드입니다.

 

void printWifiStatus() {
  // print the SSID of the network you're attached to:
  Serial.print("SSID: ");
  Serial.println(WiFi.SSID());

  // print your board's IP address:
  IPAddress ip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(ip);

  // print the received signal strength:
  long rssi = WiFi.RSSI();
  Serial.print("signal strength (RSSI):");
  Serial.print(rssi);
  Serial.println(" dBm");
}

 

데모

 

Arduino IDE 직렬 모니터를 9600의 전송 속도로 열면 60 초마다 직렬 모니터에 온도 (켈빈)와 습도(퍼센트)가 인쇄되는 것을 볼 수 있습니다.

 

 

텍스트로 나타내면 아래와 같습니다. 결과는 너무 좋게 나옵니다. ^^

 

Serial ready
Attempting to connect to SSID: SK_WiFiGIGAA988
Connected to wifi
SSID: SK_WiFiGIGAA988
IP Address: 192.168.35.75
signal strength (RSSI):-68 dBm
Connect to api.openweathermap.org
Connected
GET /data/2.5/weather?q=seoul,KR&APPID=34b3527445f4bb5a2b8be73f3a8244b0
Temp = 288.56
Humidity = 59
Disconnect
Wait 60 seconds
Connect to api.openweathermap.org
Connected
GET /data/2.5/weather?q=seoul,KR&APPID=34b3527445f4bb5a2b8be73f3a8244b0
Temp = 288.56
Humidity = 59
Disconnect
Wait 60 seconds
Connect to api.openweathermap.org

 

OpenWeatherMap API 응답의 나머지 정보에 액세스 할 수 있지만 데모 목적으로 온도와 습도만 디코딩했습니다.

 

위 잘나오는 결과를 볼 수 있는 전체 코드를 아래에 올립니다. Nano 33 IoT 보드에 맞게 수정해서 적용한 코드라서 1편 포스팅만 따라했다면 제대로 돌아가는 코드입니다. 

 

/*
 * Rui Santos 
 * Complete Project Details http://randomnerdtutorials.com
 * Based on the Arduino Ethernet Web Client Example
 * and on the sketch "Sample Arduino Json Web Client" of the Arduino JSON library by Benoit Blanchon (bblanchon.github.io/ArduinoJson)
 */

#include <ArduinoJson.h>
#include <Ethernet.h>
#include <SPI.h>
#include <SPI.h>
#include <WiFiNINA.h>

//EthernetClient client;
WiFiClient client;

// Name address for Open Weather Map API
const char* server = "api.openweathermap.org";

// Replace with your unique URL resource
const char* resource = "/data/2.5/weather?q=seoul,KR&APPID=34b3527445f~~~~~~~~~~~~~~~~~";

// How your resource variable should look like, but with your own COUNTRY CODE, CITY and API KEY (that API KEY below is just an example):
//const char* resource = "/data/2.5/weather?q=Porto,pt&appid=bd939aa3d23ff33d3c8f5dd1";

const unsigned long HTTP_TIMEOUT = 10000;  // max respone time from server
const size_t MAX_CONTENT_SIZE = 512;       // max size of the HTTP response


#include "arduino_secrets.h" 
///////please enter your sensitive data in the Secret tab/arduino_secrets.h
char ssid[] = SECRET_SSID;        // your network SSID (name)
char pass[] = SECRET_PASS;    // your network password (use for WPA, or use as key for WEP)
int keyIndex = 0;            // your network key Index number (needed only for WEP)

int status = WL_IDLE_STATUS;

// The type of data that we want to extract from the page
struct clientData {
  char temp[8];
  char humidity[8];
};

// ARDUINO entry point #1: runs once when you press reset or power the board
void setup() {
  Serial.begin(9600);
  while (!Serial) {
    ;  // wait for serial port to initialize
  }
  
  Serial.println("Serial ready");
  while (status != WL_CONNECTED) {
    Serial.print("Attempting to connect to SSID: ");
    Serial.println(ssid);
    // Connect to WPA/WPA2 network. Change this line if using open or WEP network:
    status = WiFi.begin(ssid, pass);

    // wait 10 seconds for connection:
    delay(10000);
  }
  Serial.println("Connected to wifi");
  printWifiStatus();
}

// ARDUINO entry point #2: runs over and over again forever
void loop() {
  if(connect(server)) {
    if(sendRequest(server, resource) && skipResponseHeaders()) {
      clientData clientData;
      if(readReponseContent(&clientData)) {
        printclientData(&clientData);
      }
    }
  }
  disconnect();
  wait();
}

// Open connection to the HTTP server
bool connect(const char* hostName) {
  Serial.print("Connect to ");
  Serial.println(hostName);

  bool ok = client.connect(hostName, 80);

  Serial.println(ok ? "Connected" : "Connection Failed!");
  return ok;
}

// Send the HTTP GET request to the server
bool sendRequest(const char* host, const char* resource) {
  Serial.print("GET ");
  Serial.println(resource);

  client.print("GET ");
  client.print(resource);
  client.println(" HTTP/1.1");
  client.print("Host: ");
  client.println(host);
  client.println("Connection: close");
  client.println();

  return true;
}

// Skip HTTP headers so that we are at the beginning of the response's body
bool skipResponseHeaders() {
  // HTTP headers end with an empty line
  char endOfHeaders[] = "\r\n\r\n";

  client.setTimeout(HTTP_TIMEOUT);
  bool ok = client.find(endOfHeaders);

  if (!ok) {
    Serial.println("No response or invalid response!");
  }
  return ok;
}

// Parse the JSON from the input string and extract the interesting values
// Here is the JSON we need to parse
/*{
    "coord": {
        "lon": -8.61,
        "lat": 41.15
    },
    "weather": [
        {
            "id": 800,
            "main": "Clear",
            "description": "clear sky",
            "icon": "01d"
        }
    ],
    "base": "stations",
    "main": {
        "temp": 296.15,
        "pressure": 1020,
        "humidity": 69,
        "temp_min": 296.15,
        "temp_max": 296.15
    },
    "visibility": 10000,
    "wind": {
        "speed": 4.6,
        "deg": 320
    },
    "clouds": {
        "all": 0
    },
    "dt": 1499869800,
    "sys": {
        "type": 1,
        "id": 5959,
        "message": 0.0022,
        "country": "PT",
        "sunrise": 1499836380,
        "sunset": 1499890019
    },
    "id": 2735943,
    "name": "Porto",
    "cod": 200
}*/

bool readReponseContent(struct clientData* clientData) {
  // Compute optimal size of the JSON buffer according to what we need to parse.
  // See https://bblanchon.github.io/ArduinoJson/assistant/
  const size_t bufferSize = JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(1) + 
      2*JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(4) + JSON_OBJECT_SIZE(5) + 
      JSON_OBJECT_SIZE(6) + JSON_OBJECT_SIZE(12) + 390;
  DynamicJsonBuffer jsonBuffer(bufferSize);

  JsonObject& root = jsonBuffer.parseObject(client);

  if (!root.success()) {
    Serial.println("JSON parsing failed!");
    return false;
  }

  // Here were copy the strings we're interested in using to your struct data
  strcpy(clientData->temp, root["main"]["temp"]);
  strcpy(clientData->humidity, root["main"]["humidity"]);
  // It's not mandatory to make a copy, you could just use the pointers
  // Since, they are pointing inside the "content" buffer, so you need to make
  // sure it's still in memory when you read the string

  return true;
}

// Print the data extracted from the JSON
void printclientData(const struct clientData* clientData) {
  Serial.print("Temp = ");
  Serial.println(clientData->temp);
  Serial.print("Humidity = ");
  Serial.println(clientData->humidity);
}

// Close the connection with the HTTP server
void disconnect() {
  Serial.println("Disconnect");
  client.stop();
}

// Pause for a 1 minute
void wait() {
  Serial.println("Wait 60 seconds");
  delay(60000);
}


void printWifiStatus() {
  // print the SSID of the network you're attached to:
  Serial.print("SSID: ");
  Serial.println(WiFi.SSID());

  // print your board's IP address:
  IPAddress ip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(ip);

  // print the received signal strength:
  long rssi = WiFi.RSSI();
  Serial.print("signal strength (RSSI):");
  Serial.print(rssi);
  Serial.println(" dBm");
}

 

좀 길지 않나요? 소스코드도 복잡하고요. 다음 포스팅에서 "JSON 인코딩 – JSON 문자열 생성"을 올리겠습니다. 어떻게 하면 과거를 끊을 수 있을까요? 매일 아침이 갓 태어난 아기처럼 살아가려면요? 마치 강아지나 고양이처럼 매 순간을 산다면요. 그런 방법은 사실 없지요. 과거를 잊는 겁니다. 가장 가까이 다가갈 방법은 무엇인지 생각해 봅니다. 

 

다음 포스팅은 Encoding JSON – Generate JSON string 에 대해 올립니다.  

 

 

 

 

 

반응형