ArduinoでJsonを生成、解析する

# はじめに
`Arduino Jsonライブラリ`はArduinoで動作できることができる軽量のJson解析、生成を行えるライブラリです。

`ArduinoJson`はAPIから取得したデータの解析だけでなく、SDカードにJson形式で保存する際にも利用できます。

https://arduinojson.org/

# インストール
Arudino IDEの場合、ライブラリマネージャに`ArduinoJson`と検索すれば出てきます。

# 利用方法

### ライブラリの宣言
これは当たり前です。
```C++
#include <ArduinoJson.h>
```

と宣言すればいいです。

### 領域を確保する
Jsonを読み込む方法は2つあります。
```C++
StaticJsonDocumet<確保する大きさ>　変数名;
```
と
```C++
DynamicJsonDocument 変数名(確保する大きさ);
```

です。それぞれ、静的に領域を確保するか、動的に領域を確保するかの違いですが、最も大きく違うのが**確保する領域の場所**です。

**`StaticJsonDocumet`メソッドはスタック領域**に確保します

**`DynamicJsonDocumet`メソッドはヒープ領域**に確保します

日本語を処理する場合、一文字3バイトもあるのでヒープがすぐにいっぱいになって連続再起動が起こります。また、**メモリ断片化**にも注意が必要です。


## Jsonを生成する
### キーと値のペアを追加する方法

```C++
doc["キー"] = 値;
```
で生成できます。

Jsonはキーと値のペアで構成されています。
適当なJsonを覗くと
```Json
"キー": 値
```
という形式で保存されています。

さらに具体的に言うと
```Json
{
  "name": "John Doe",
  "age": 30,
  "isStudent": false
}
```
こういった感じになっています。
その一列を作成するということです。

上記のJsonを`ArduinoJson`で利用する場合
```Json
doc["name"] = "John Doe";
doc["age"] = 30;
doc["isStudent"] = false;
```
という感じになります。


#### 配列の作成
ArduinoJsonには**配列を作成するメソッド**があります。
```cpp
JsonArray data = doc.createNestedArray("キー");
```

これはJsonに以下のような行を追加することができます。

```cpp
{
  "キー": []
}
```

##### 配列に値を入れる
配列に値を入れるには以下の方法があります。
```cpp
data.add(値);
```
これは、先ほど作成した**配列に値を入れるメソッド**です。

```cpp
JsonArray value = doc.createNestedArray("Value")
data.add(1);
data.add(2);
```
をすると以下のようなJsonを得られます。

```json
{ "Value": [1, 2] }
```

##### 配列に別のキーを挿入する
配列に別のキーに挿入する場合、
```cpp
JsonObject location = doc.createNestedObject("配列のキー"); 
location["配列に追加するキー1"] = 値; 
location["配列に追加するキー2"] = 値;
```

生成されるJsonは以下のようになります。
```Json
{
  "配列のキー": {
    "配列に追加するキー1": 値,
    "配列に追加するキー2": 値
  }
}
```

#### 値の挿入まとめ

値の型：
    - 文字列： `doc["name"] = "John Doe";`
    - 数値： `doc["age"] = 30;`
    - ブール値： `doc["isStudent"] = false;`
    - null： `doc["middleName"] = nullptr;`
    - オブジェクト： `JsonObject address = doc.createNestedObject("address");`
    - 配列： `JsonArray hobbies = doc.createNestedArray("hobbies");`

などです。これらを利用すると以下のようなものを作成できます

C++
```C++
doc["name"] = "John Doe";
doc["age"] = 30;
JsonArray hobbies = doc.createNestedArray("hobbies");
hobbies.add("reading");
hobbies.add("cycling");
JsonObject address = doc.createNestedObject("address");
address["street"] = "123 Main St";
address["city"] = "Anytown";
```

生成されるJson
```Json
{
  "name": "John Doe",
  "age": 30,
  "hobbies": ["reading", "cycling"],
  "address": {
    "street": "123 Main St",
    "city": "Anytown"
  }
}
```

## Jsonを表示する
これまではJsonを生成するメソッドを説明しましたが、ここからは生成したJsonを表示するメソッドについて解説します。


##### シリアライズする
表示するためにはシリアライズする必要があります。

	シリアライズとは、C++ のオブジェクトや変数を JSON 形式の文字列に変換するプロセス

のことです。

```cpp
serializeJson(doc, output); //整形なし
serializeJsonPretty(doc, output);//整形あり(メモリ消費は増大します)
```

いままで生成したJsonを出力します。

```cpp
serializeJson(doc, output);
```
	docはシリアライズするjsonドキュメント
	outputはシリアライズされたjsonを格納する出力先

###### String形の変数に格納する場合
```cpp
String jsonString;
serializeJson(doc, jsonString);
```

###### char型の配列へ出力
```cpp
size_t capacity = measureJson(doc);//jsonのバッファサイズを計算
char jsonBuffer[capacity];
size_t len = serializeJson(doc, jsonBuffer);
```
バッファサイズを指定してシリアライズします。

シリアライズされたJsonはserialに表示されます。


## Jsonを取得する
### メモリの確保とデシリアライズ
取得する際には、まずメモリ領域を確保する必要があります。

```C++
DynamicJsonDocument doc(確保する大きさ)
```

その後、文字列をパース(解析)する必要があります。

```C++
deserializeJson(doc, jsonString);//エラーチャックを行わない
deserializeJsonError error = deserializeJson(doc, jsonString);//エラーチェックあり
```

`JsonString`以外にもオプションがあります。

重要)`payload`　オブジェクト:
- APIからJsonを取得した場合に使用します。
- APIは基本的にpayloadに保存されているためpayloadを取得します。

a) ストリーム（Stream）オブジェクト:

- ファイル（SDカードなど）やシリアル通信からの読み込みに使用します。
- 例: `deserializeJson(doc, file);`

b) クライアントオブジェクト（Client）:

- ネットワーククライアント（WiFiClient, EthernetClientなど）からの読み込みに使用します。
- 例: `deserializeJson(doc, client);`

c) バイト配列（uint8_t[]）:

- メモリ上のバイト配列からJSONを解析します。
- 例: `deserializeJson(doc, jsonBuffer, bufferSize);`

d) `std::istream`:

- C++の標準入力ストリームからの読み込みに使用します（通常はArduino環境では使用しません）。

e) `ArduinoJson::VariantData`オブジェクト:

- 高度な使用例で、既存のJSONドキュメントのコピーを作成する際に使用します。

##### 使用例
```cpp
#include <ArduinoJson.h>

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

  const char* jsonString = R"({"sensor":"温度計","value":25.5})";

  DynamicJsonDocument doc(1024);

  // 方法1: エラーチェックなし
  deserializeJson(doc, jsonString);

  // 方法2: エラーチェックあり
  DeserializationError error = deserializeJson(doc, jsonString);

  if (error) {
    Serial.print("デシリアライゼーション失敗: ");
    Serial.println(error.c_str());
    return;
  }

  // ストリームからの読み込み例（ファイルやシリアル通信など）
  // File file = SD.open("data.json");
  // DeserializationError error = deserializeJson(doc, file);

  // ネットワーククライアントからの読み込み例
  // WiFiClient client;
  // DeserializationError error = deserializeJson(doc, client);

  // バイト配列からの読み込み例
  uint8_t jsonBuffer[] = {123, 34, 115, 101, 110, 115, 111, 114, 34, 58, 34, 228, 189, 147, 229, 186, 166, 232, 168, 136, 34, 125};
  DeserializationError error = deserializeJson(doc, jsonBuffer, sizeof(jsonBuffer));

  // 結果の表示
  serializeJsonPretty(doc, Serial);
}

void loop() {
  // ここでは何もしない
}
```

### 値を取得する

ここでは以下のJsonを解析する際の方法について検討します。
```Json
    {
      "sensor": "温度センサー",
      "timestamp": 1628097600,
      "temperature": 25.5,
      "isActive": true,
      "tags": ["indoor", "celsius"],
      "location": {
        "room": "リビング",
        "floor": 1
      },
      "readings": [
        {"time": "09:00", "value": 23.1},
        {"time": "12:00", "value": 25.5},
        {"time": "15:00", "value": 24.3}
      ]
    }
```
このJsonを解析します

**注意:文字列を取得する際、`const char*`を使用すると、元のJSONドキュメントが存在する間だけ有効です。長期保存には`String`クラスを使用してください。**

##### 基本的な値を取得する
```cpp
doc["キー名"];
```
で取得できます。

`const char* sensor`変数に`sensor`キーの値保存する場合は、
```cpp
const char* sensor = doc["sensor"];
```

##### 配列の要素を取得する
```cpp
doc["array"][index]
```
で取得できます。

```cpp
const char* firstTag = doc["tags"][0];
```

`tags`キーの最初の値を取得します。

##### ネストされたオブジェクトの取得
ネストとは
```Json
"location": { 
"room": "リビング",
"floor": 1 
}
```

こういった形式のJsonの要素のことです。

```cpp
doc["parent"]["child"]
```
で取得できます。

	`parents`はlocationのことです。`child`はroomやfloorのことです

roomを取得する場合は
```cpp
const char* room = doc["location"]["room"];
```

で取得できます。

##### 配列内のオブジェクトの値を取得
配列内のオブジェクトを取得します。
###### 特定の値を取得する場合
```cpp
doc["array"][index]["key"]
```

例
```cpp
float firstReading = doc["readings"][0]["value"];
```


##### そのほかの取得+例のコード
```cpp
#include <ArduinoJson.h>

void setup() {
  Serial.begin(9600);
  while (!Serial) continue;

  // サンプルJSONデータ
  const char* jsonString = R"(
    {
      "sensor": "温度センサー",
      "timestamp": 1628097600,
      "temperature": 25.5,
      "isActive": true,
      "tags": ["indoor", "celsius"],
      "location": {
        "room": "リビング",
        "floor": 1
      },
      "readings": [
        {"time": "09:00", "value": 23.1},
        {"time": "12:00", "value": 25.5},
        {"time": "15:00", "value": 24.3}
      ]
    }
  )";

  DynamicJsonDocument doc(1024);
  DeserializationError error = deserializeJson(doc, jsonString);

  // エラー処理の追加
  if (error) {
    Serial.print(F("JSONのパースに失敗: "));
    Serial.println(error.f_str());
    return;
  }

  // 1. 基本的な値の取得（型チェックとデフォルト値の使用）
  const char* sensor = doc["sensor"] | "不明なセンサー";
  long timestamp = doc["timestamp"] | 0L;
  float temperature = doc["temperature"] | -273.15f; // 絶対零度をデフォルトに
  bool isActive = doc["isActive"] | false;

  Serial.println(F("基本的な値:"));
  Serial.println(sensor);
  Serial.println(timestamp);
  Serial.println(temperature);
  Serial.println(isActive ? F("アクティブ") : F("非アクティブ"));

  // 2. 配列の要素を取得（境界チェックの追加）
  JsonArray tags = doc["tags"];
  Serial.println(F("\n配列の要素:"));
  if (tags.size() > 0) Serial.println(tags[0].as<const char*>());
  if (tags.size() > 1) Serial.println(tags[1].as<const char*>());

  // 3. ネストされたオブジェクトの値を取得（存在チェックの追加）
  JsonObject location = doc["location"];
  Serial.println(F("\nネストされたオブジェクト:"));
  if (location.containsKey("room")) Serial.println(location["room"].as<const char*>());
  if (location.containsKey("floor")) Serial.println(location["floor"].as<int>());

  // 4. 配列内のオブジェクトの値を取得（エラーチェックの追加）
  JsonArray readings = doc["readings"];
  Serial.println(F("\n配列内のオブジェクト:"));
  if (readings.size() > 0 && readings[0].containsKey("value"))
    Serial.println(readings[0]["value"].as<float>());
  if (readings.size() > 1 && readings[1].containsKey("time"))
    Serial.println(readings[1]["time"].as<const char*>());

  // 5. 配列の全要素をイテレート
  Serial.println(F("\n配列のイテレーション:"));
  for (JsonVariant value : tags) {
    Serial.println(value.as<String>());
  }

  // 6. オブジェクトの全キーと値をイテレート
  Serial.println(F("\nオブジェクトのイテレーション:"));
  for (JsonPair kv : location) {
    Serial.print(kv.key().c_str());
    Serial.print(F(": "));
    Serial.println(kv.value().as<String>());
  }

  // 7. 存在しないキーの安全な取得
  int unknownValue = doc["unknown"] | -1;  // デフォルト値 -1
  Serial.println(F("\n存在しないキー:"));
  Serial.println(unknownValue);

  // 8. 型を指定して値を取得（エラーチェックの追加）
  Serial.println(F("\n型を指定した取得:"));
  if (doc["sensor"].is<const char*>()) {
    String sensorStr = doc["sensor"].as<String>();
    Serial.println(sensorStr);
  } else {
    Serial.println(F("センサーの値が文字列ではありません"));
  }
}

void loop() {
  // ここでは何もしない
}
```

# 実装例
カクテルAPIからマルガリータ?の要素を取得して表示してみます。筆者はお酒が苦手なので何かわかりませんがとりあえず表示してみます。

```C++
// 必要なライブラリをインクルードします
#include <ESP8266WiFi.h>        // ESP8266のWiFi機能を使うためのライブラリ
#include <WiFiClientSecure.h>   // セキュアなWiFi接続を行うためのライブラリ
#include <ArduinoJson.h>        // JSONデータを扱うためのライブラリ

// Wi-FiのSSIDとパスワードを設定します
const char* ssid = "I";     // あなたのWi-FiのSSID
const char* password = "M";    // あなたのWi-Fiのパスワード

// 接続先のサーバー情報を設定します
const char* host = "www.thecocktaildb.com";  // 接続先のサーバーのホスト名
const int httpsPort = 443;                   // HTTPSの標準ポート番号

// セットアップ関数：プログラムの初期化を行います
void setup() {
  Serial.begin(115200);  // シリアル通信を開始（ボーレートは115200）
  Serial.println();      // 空行を出力

  // Wi-Fiへの接続を開始します
  WiFi.begin(ssid, password);
  // Wi-Fiに接続するまで待機します
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi...");
  }
  Serial.println("Connected to WiFi");

  // カクテル情報を一度だけ取得して表示します
  fetchCocktails();
}

// ループ関数：このプログラムでは特に何もしません
void loop() {
  // 空のままです
}

// カクテル情報を取得して表示する関数
void fetchCocktails() {
  // セキュアなWiFiクライアントを作成
  WiFiClientSecure client;
  client.setInsecure();  // 注意：証明書の検証をスキップします（開発用）

  // サーバーに接続
  if (!client.connect(host, httpsPort)) {
    Serial.println("Connection failed");
    return;
  }

  // APIのURLを設定
  String url = "/api/json/v1/1/search.php?s=margarita";
  
  // HTTPリクエストを送信
  client.print(String("GET ") + url + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" +
               "User-Agent: ESP8266\r\n" +
               "Connection: close\r\n\r\n");

  // HTTPヘッダーをスキップ
  if (!client.find("\r\n\r\n")) {
    Serial.println("Invalid response");
    return;
  }

  // JSONデータを解析するためのオブジェクトを作成
  DynamicJsonDocument doc(16384);
  DeserializationError error = deserializeJson(doc, client);

  // JSONの解析に失敗した場合はエラーを表示して終了
  if (error) {
    Serial.print("deserializeJson() failed: ");
    Serial.println(error.c_str());
    return;
  }

  // カクテルの情報を抽出して表示
  JsonArray drinks = doc["drinks"];
  int cocktailCount = 0;

  // 各カクテルの情報を処理して表示
  for (JsonObject drink : drinks) {
    if (cocktailCount >= 10) break;  // 最大10個まで処理

    String name = drink["strDrink"].as<String>();
    String instructions = drink["strInstructions"].as<String>();

    // 名前と作り方が存在する場合のみ処理
    if (name.length() > 0 && instructions.length() > 0) {
      Serial.println("\n--------------------");
      Serial.println("Cocktail: " + name);
      Serial.println("Instructions: " + instructions);
      
      // 材料と分量を表示
      for (int i = 1; i <= 15; i++) {
        String ingredientKey = "strIngredient" + String(i);
        String measureKey = "strMeasure" + String(i);
        
        String ingredient = drink[ingredientKey].as<String>();
        String measure = drink[measureKey].as<String>();
        
        // 材料が存在する場合のみ表示
        if (ingredient.length() > 0) {
          if (measure.length() > 0) {
            Serial.println(measure + " " + ingredient);
          } else {
            Serial.println(ingredient);
          }
        } else {
          break;  // 材料がなくなったらループを終了
        }
      }
      
      Serial.println("--------------------\n");
      cocktailCount++;  // 処理したカクテルの数を増やす
    }
  }

  Serial.println("All cocktail information displayed.");
}
```