M5Stick-CでJsonをPOSTする

M5Stick-Cを使って、HTTP POSTします。当然、BodyにJSONを入れて、JSONが返ってくるやつです。
今回は、Arduino IDEを使います。
いろんな人が使っているので情報がたくさんありますが、JsonをPOSTするまで複数の情報を集めないと実現できなかったので、ここでまとめておきます。

使うもの
・M5Stack-C
　　ESP系CPUであれば大丈夫なのですが、M5Stack-CについているLCDを使っているのでそこだけ代替してください。
・Arduino IDE
　　https://docs.m5stack.com/#/ja/quick_start/m5stickc/m5stickc_quick_start
　上記ページを参考に、ボードマネージャとライブラリマネージャよりM5StickCをインストールしておきます。

インストールが必要なライブラリ
・ArduinoJson
　　https://arduinojson.org/v6/doc/
　JSONを簡単に扱えるようになります。Arduino IDEのライブラリマネージャからインストールしておいて下さい。

本投稿の最後に、M5Stick-Cについているジャイロ、バッテリー容量、GroveのBME680の環境センサ、NTP(時刻)、Ambientへのアップロードもろもろを組み込んだサンプルソースも載せておきます。

# WiFiアクセスポイントに接続する

ESP系であれば、標準のライブラリでできます。setup()で準備します。

```c
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(100);
  }
  Serial.println(WiFi.localIP());
```

# POSTするJsonを作成する

JSONの生成には、ArduinoJsonを使いました。
まずは定義。

```c
const int capacity = JSON_OBJECT_SIZE(2);
StaticJsonDocument<capacity> json_request;
```

メモリの省エネを意識しているようで、あらかじめセットする予定の項目の数を指定する必要があります。JSON_OBJECT_SIZEの部分です。セットする項目が配列だったり、階層化されていたりする場合の計算方法もあるので、ArduinoJsonのHPを参考にしてください。
今回は2個の項目を設定します。
あるいは、必要な容量は以下のページでも計算してくれます。

ArduinoJson Assistant
　https://arduinojson.org/v6/assistant/

Jsonの項目の設定はこんな感じ。

```
  json_request["counter"] = counter;
  json_request["tick"] = tick;
```

最後に、JSON文字列化(JSON.stringifyのことです)します。
出力先にバッファー(buffer)を用意しました。

```c
char buffer[255];
serializeJson(json_request, buffer, sizeof(buffer));
```

# JsonをPOSTする

POSTには、HTTPClientを使いました。

以下の感じです。

```c
  HTTPClient http;
  http.begin(host);
  http.addHeader("Content-Type", "application/json");
  int status_code = http.POST((uint8_t*)buffer, strlen(buffer));
  if( status_code == 200 ){
    Stream* resp = http.getStreamPtr();

    DynamicJsonDocument json_response(255);
    deserializeJson(json_response, *resp);

    serializeJson(json_response, Serial);
    Serial.println("");
  }
  http.end();
```

http.POSTで、JSON文字列を載せてPOSTしています。

結果は、http.getStreamPtr()で取得します。
WiFiClient* が返ってくるのですが、Streamを継承しています。

POSTのレスポンスは、application/jsonであることを期待して、パースしています。
以下の部分で、JSON文字列をパースしています。

```c
    DynamicJsonDocument json_response(255);
    deserializeJson(json_response, *resp);
```

（もちろん、DynamicJsonDocumentではなくStaticJsonDocumentを使っても構いません。）

値を取り出すときにはいったん変数に取り出すのが良いです。
そうすることで、変数の型に合わせて自動的に変換してくれます。
文字列を取り出すときには、constをつけるようにした方がメモリ効率上良いようです。

```
int status = json_response["status"];
const char* message = json_response["message"];
```

# ソースコード

以上をまとめたソースコードは以下になります。

```c
#include <M5StickC.h>
#include <WiFi.h>
#include <WiFiMulti.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

const char *ssid = "【WiFiアクセスポイントのSSID】";
const char *password = "【WiFiアクセスポイントのパスワード】";

const int capacity = JSON_OBJECT_SIZE(2);
StaticJsonDocument<capacity> json_request;
char buffer[255];

const char *host = "【POST接続するエンドポイント】";

unsigned long counter = 0;
unsigned long tick = 0;

void setup() {
  M5.begin();

  M5.Axp.ScreenBreath(9);
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextSize(2);

  M5.Lcd.println("[M5StickC]");
  delay(1000);

  M5.Lcd.println("start Serial");
  Serial.begin(9600);
  while (!Serial);
  delay(100);

  M5.Lcd.println("start WiFi");
  Serial.print("start Wifi");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(100);
  }
  Serial.println("");
  M5.Lcd.println("Connected");
}

void loop() {
  counter++;
  tick = millis();
  
  json_request["counter"] = counter;
  json_request["tick"] = tick;
  
  serializeJson(json_request, Serial);
  Serial.println("");

  serializeJson(json_request, buffer, sizeof(buffer));

  HTTPClient http;
  http.begin(host);
  http.addHeader("Content-Type", "application/json");
  int status_code = http.POST((uint8_t*)buffer, strlen(buffer));
  Serial.printf("status_code=%d\r\n", status_code);
  if( status_code == 200 ){
    Stream* resp = http.getStreamPtr();

    DynamicJsonDocument json_response(255);
    deserializeJson(json_response, *resp);

    serializeJson(json_response, Serial);
    Serial.println("");
  }
  http.end();

  delay(5000);
}
```

以下の部分は、各自の環境に合わせて変更してください。

【WiFiアクセスポイントのSSID】
【WiFiアクセスポイントのパスワード】
【POST接続するエンドポイント】

# (参考)サンプルアプリ

ちょうど手元に、GroveのBME680があったので、つないでみました。

Grove - Temperature Humidity Pressure Gas Sensor(BME680)
　http://wiki.seeedstudio.com/Grove-Temperature_Humidity_Pressure_Gas_Sensor_BME680/

また、せっかくなので、Ambientを使わせていただいて、可視化してみました。

Ambient
　https://ambidata.io/

Arduino IDEに以下をインストールしておいて下さい。
　https://github.com/AmbientDataInc/Ambient_ESP8266_lib
　https://github.com/Seeed-Studio/Seeed_BME680

ソースコードは以下の通りです。

```c
#include <M5StickC.h>
#include <WiFi.h>
#include <WiFiMulti.h>
#include <WiFiUdp.h>
#include <NTPClient.h>

#define BME6800

#ifdef BME6800

#include "seeed_bme680.h"

#include "Ambient.h"

WiFiClient client;
Ambient ambient;
unsigned int channelId = 【AmbientのチャネルID】;
const char* writeKey = "【Ambientのライトキー】";

#define BME_SCK 13
#define BME_MISO 12
#define BME_MOSI 11
#define BME_CS 10

#define IIC_ADDR  uint8_t(0x76)

/**  NOTE!!!!!!!!!!!!  Select the communication protocol correctly **/

Seeed_BME680 bme680(IIC_ADDR); /* IIC PROTOCOL */
//Seeed_BME680 bme680;             /* SPI PROTOCOL */
//Seeed_BME680 bme680(BME_CS, BME_MOSI, BME_MISO,  BME_SCK);/*SPI PROTOCOL*/

#define MAX_COUNT_ENV   5

#endif

const char *ssid = "【WiFiアクセスポイントのSSID】";
const char *password = "【WiFiアクセスポイントのパスワード】";

bool ntp_updated = false;
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, 9 * 60 * 60);

#define TIME_INTARVAL   1000
unsigned long next_time = TIME_INTARVAL;
unsigned long prev_time = 0;

#define MAX_COUNT_VBAT   10
unsigned long count = 0;
#define CHECK_COUNT(c,m)  (c != 0 && count < (m))

enum{
  MODE_GYRO = 0,
#ifdef BME6800
  MODE_ENV,
#endif
  MODE_VBAT,
  MODE_TIME,
  
  NUM_OF_MODE
};
unsigned char mode = MODE_GYRO;

void setup() {
  M5.begin();
  M5.IMU.Init();
  
  M5.Axp.ScreenBreath(9);
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextSize(2);

  M5.Lcd.println("[M5StickC]");
  delay(1000);

  M5.Lcd.println("start Serial");
  Serial.begin(9600);
  while (!Serial);
  delay(100);

  M5.Lcd.println("start WiFi");
  Serial.print("start Wifi");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(100);
  }
  Serial.println("");

  timeClient.begin();

#ifdef BME6800
  M5.Lcd.println("start BME680");
  while (!bme680.init()){
    Serial.println("bme680 init failed ! can't find device!");
    delay(10000);
  }

  ambient.begin(channelId, writeKey, &client);
#endif
}

void loop() {
  M5.update();
  
  if( !ntp_updated ){
    Serial.println("NTP updating");
    ntp_updated = timeClient.update();
    if( ntp_updated ){
      Serial.println("NTP updated");
    }
  }

  if( M5.BtnA.wasReleased() ){
    mode = (mode + 1) % NUM_OF_MODE;
    count = 0;
  }

  if( count > 0 ){
    unsigned long now = millis();
    if( ( ( next_time > prev_time ) && ( prev_time < now && now <= next_time ) ) ||
        ( ( next_time < prev_time ) && ( prev_time < now || now <= next_time ) ) )
      return;
  
    prev_time = next_time;
    next_time += TIME_INTARVAL;

    count++;
  }

  if( mode == MODE_GYRO ){
    int16_t accX, accY, accZ;
    int16_t gyroX, gyroY, gyroZ;
    M5.IMU.getGyroAdc(&gyroX, &gyroY, &gyroZ);
    M5.IMU.getAccelAdc(&accX, &accY, &accZ);

    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.setTextSize(1);

    M5.Lcd.printf("gyrX: %.2f\n", ((float)gyroX) * M5.IMU.gRes);
    M5.Lcd.printf("gyrY: %.2f\n", ((float)gyroY) * M5.IMU.gRes);
    M5.Lcd.printf("gyrZ: %.2f\n", ((float)gyroZ) * M5.IMU.gRes);
    M5.Lcd.printf("accX: %.2f\n", ((float)accX) * M5.IMU.gRes);
    M5.Lcd.printf("accY: %.2f\n", ((float)accY) * M5.IMU.gRes);
    M5.Lcd.printf("accZ: %.2f\n", ((float)accZ) * M5.IMU.gRes);
  }else
#ifdef BME6800
  if( mode == MODE_ENV ){
    if(CHECK_COUNT(count, MAX_COUNT_ENV))
      return;
  
    if (bme680.read_sensor_data()){
      Serial.println("Failed to perform reading :(");
      return;
    }
  
    float tmp, hum, pres, gas;
    
    tmp = bme680.sensor_result_value.temperature;
    hum = bme680.sensor_result_value.humidity;
    pres = bme680.sensor_result_value.pressure / 100.0;
    gas = bme680.sensor_result_value.gas / 1000.0;

    Serial.printf("tmp: %2.1fC\r\n", tmp);
    Serial.printf("hum: %2.1f%%\r\n", hum);
    Serial.printf("prs: %3.0fhPa\r\n", pres);
    Serial.printf("gas: %2.0fKohm\r\n", gas);
    Serial.println("");

    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.setTextSize(2);
    M5.Lcd.printf("tmp: %2.1fC\n", tmp);
    M5.Lcd.printf("hum: %2.1f%%\n", hum);
    M5.Lcd.printf("prs: %3.0fhPa\n", pres);
    M5.Lcd.printf("gas: %2.0fKohm\n", gas);

    ambient.set(1, String(tmp).c_str());
    ambient.set(2, String(hum).c_str());
    ambient.set(3, String(pres).c_str());
    ambient.set(4, String(gas).c_str());
    ambient.send();
  }else
#endif  
  if( mode == MODE_VBAT ){
    if(CHECK_COUNT(count, MAX_COUNT_VBAT))
      return;

    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setCursor(0, 0);

    double vbat = M5.Axp.GetVbatData() * 1.1 / 1000;
    M5.Lcd.setTextSize(3);
    M5.Lcd.printf("v_bat:\r\n%4.2f", vbat);
  }else if( mode == MODE_TIME ){
    String time_str = timeClient.getFormattedTime();

    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.setTextSize(3);

    M5.Lcd.println(time_str);
  }

  count = 1;
}
```

以下の部分は、各自の環境に合わせて変更してください。

【WiFiアクセスポイントのSSID】
【WiFiアクセスポイントのパスワード】
【POST接続するエンドポイント】
【AmbientのチャネルID】
【Ambientのライトキー】

5秒ごとに、AmbientにBME680から取得した環境情報を上げています。
Ambientのページを見てみると、こんな感じで可視化されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/b9ab894d-294d-5d1f-95d2-67c21347cd47.png)

以上