M5StickC で kintone に環境計測データをアップする時計を作る

## 概要

M5StickC を単体で注文したつもりが、誤って腕時計マウンタ付きキットを購入してしまいました。（汗）
時を同じくして、kintone 関連のイベントで LT をすることになったので、kintone 環境計測時計（略して「KK時計」）を作ってみることにしました。（笑）

## 要求スペック

「KK時計」の要求スペックは以下に設定します。

・インターネットにはwifiで接続する
・ntpサーバから正確な時間を取得し、時分単位で表示
・1分毎に環境センサから温湿度、気圧データを取得し表示
・5分毎に温湿度、気圧データをkintoneにアップ
・なるべく省電力設計し、バッテリーで長く稼働させる

## kintone の準備

以下のようなkintoneアプリを準備します。
![M5StackC.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/ce131ea6-601a-33b5-9b26-6679289a1e17.png)
「日時」は日時の自動セットで設定すると良いでしょう。（計測時間の数秒程度の誤差は許容範囲とします。）
「場所」「MACアドレス」「CO2」「バッテリー残量」などの項目はなくて良いです。
「温度」「湿度」「気圧」「電圧」は数値で設定します。
APIトークンを「レコード追加」のアクセス権限で設定します。

## M5StickCの開発

M5StickCの開発環境と温度、湿度、気圧を測るプログラムの作成は、以下を参照ください。

M5StickCで小型環境センサ端末を作る
https://ambidata.io/samples/m5stack/m5sitckc/

※実際のファイルの拡張子は ino ですが、コードを見やすくするため c で表記しています。

``` KKClock.c
#include <M5StickC.h>
#include <WiFi.h>
#include <Wire.h>
#include <time.h>
#include <HTTPClient.h>

#include "DHT12.h"
#include "Adafruit_Sensor.h"
#include "Adafruit_BMP280.h"

// wifiの設定
const char* ssid       = "wifiのSSID";
const char* password   = "0wifiのパスワード";

// 時計の設定
const char* ntpServer =  "ntp.jst.mfeed.ad.jp";
const long  gmtOffset_sec = 9 * 3600;
const int   daylightOffset_sec = 0;

#define INTERVAL 58
RTC_DATA_ATTR static uint8_t seq = 1;

Adafruit_BMP280 bme;
DHT12 dht12;

void setup() {

  M5.begin();
  Serial.begin(115200);
  Wire.begin();

  M5.Axp.ScreenBreath(9);
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(BLACK);
  
  WiFi.begin(ssid, password);
  int i = 0;
  while (WiFi.status() != WL_CONNECTED) {
    if(i > 10) break;
    delay(100);
    i++;
  }
  char macAd[24];
  byte mac[6];
  WiFi.macAddress(mac);
  sprintf(macAd, "%02X-%02X-%02X-%02X-%02X-%02X", mac[5], mac[4], mac[3], mac[2], mac[1], mac[0]);
  Serial.printf("MAC: %s\n", macAd);
  delay(2000);

  // 時刻を取得
  M5.Lcd.setCursor(0, 0, 1);
  M5.Lcd.setTextColor(YELLOW);
  M5.Lcd.setTextSize(4);
  Serial.println(WiFi.localIP());
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) {
    M5.Lcd.println("Error!");
  }else{
    if (timeinfo.tm_hour < 10) M5.Lcd.print("0");
    M5.Lcd.print(timeinfo.tm_hour);
    M5.Lcd.print(":");
    if (timeinfo.tm_min < 10) M5.Lcd.print("0");
    M5.Lcd.println(timeinfo.tm_min);
  }

  // 温湿度を取得
  float tmp = dht12.readTemperature();
  float hum = dht12.readHumidity();

  // 気圧を取得
  i = 0;
  while (!bme.begin(0x76)){  
    if(i > 10) break;
    delay(100);
    i++;
  }
  float pressure = bme.readPressure() / 100;
  if(pressure < 200){
    pressure = 0;
  }
  
  // 電圧を取得
  double vbat = M5.Axp.GetVbatData() * 1.1 / 1000;

  // 計測結果を表示
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(2);
  Serial.printf("T:%2.1f H:%2.0f% P:%2.0f V:%4.2lf\r\n", tmp, hum, pressure, vbat);
  M5.Lcd.printf("T:%2.1f H:%2.1f%\r\nP:%2.0f V:%4.2lf\r\n", tmp, hum, pressure, vbat);

  // 計測の5回に1回kintone にデータをアップ
  if((seq % 5) == 1){
    char json[256];
    sprintf(json, "{\"app\":\"154\", \"record\":{\"place\":{\"value\":\"M5Stack-C EvaCamp\"},\"mac\":{\"value\":\"%s\"},\"temp\":{\"value\":\"%2.1f\"},\"humi\":{\"value\":\"%2.1f\"},\"pressure\":{\"value\":\"%2.0f\"},\"voltage\":{\"value\":\"%4.2lf\"}}}", macAd, tmp, hum, pressure, vbat);
    Serial.println(json);
  
    int responseCode = 0;
    HTTPClient http;
    http.begin("https://kintoneのサブドメイン.cybozu.com/k/v1/record.json");
    http.addHeader("X-Cybozu-API-Token", "kintoneのAPIトークン");
    http.addHeader("Content-type", "application/json");
    responseCode = http.POST(json);
    Serial.printf("http Response Code = %d \n", responseCode);
    String payload = http.getString();
    Serial.println(payload);
    M5.Lcd.printf("Res: %d", responseCode);
  }
  
  WiFi.disconnect();
  seq++;
  delay(3000);
  M5.Axp.ScreenBreath(8);

  // INTERVAL 秒 Deep Sleep
  esp_deep_sleep(1000000LL * INTERVAL); 
  // 以降は処理しない
}

void loop() {
}
```

### 要点１：HTTPClient を利用して kintone API 処理
M5StickC で kintone API などの Web API を実行するライブラリィは HTTPClient がお勧めです。
データのPOSTなどの際は簡単にヘッダーを addHeader() で追加可能で、レスポンスコードは POST() の戻り値として取得できます。
また、JSONなどの戻り値も getString() で簡単に取得できます。

WiFiClient でも Web API の実装は可能ですが、HTTPのヘッダーなどを write() で全て出力する必要があり、実装が面倒になりますので HTTPClient が断然お勧めです。

### 要点２：処理のインターバル間は消費電力を抑える
M5StackCを省エネ運用するため、処理の間は esp_deep_sleep() で最小限の消費電力で稼働させています。
液晶は明るいまま表示を続けると電力を消費しますので、esp_deep_sleep() の前に M5.Axp.ScreenBreath() で少し輝度を落としています。
iPhoneでデザリングして気づいたのですが、参考にした「M5StickCで小型環境センサ端末を作る」では wifi 接続のセッションがキープされてしまうようです。
省電力のために esp_deep_sleep() の前に WiFi.disconnect() を実行します。

## 結果
1分毎に時刻を更新し、5分毎にkintoneに環境計測情報をアップできました。
ですが、後日に問題点に記述したトラブルが発生しました。
さらに、イベント当日は気圧センサの値が取れないトラブルが発生しました。

試験用としては十分なんですが、センサは気圧のみ利用するか、温湿度のみ利用でないと実用には厳しいです。
センサで実用を目指すなら M5Stack の方を選択すべきかもしれません。

### 計測結果表示
![IMG_5479.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/c408a17b-cc53-fe70-e1ff-bc6185d4bf6c.jpeg)

### インターバル中
若干液晶が暗くなっているのがわかると思います。
![IMG_5480.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/34dd2f53-bc92-9382-1138-f948b2748c2c.jpeg)

### kintone 開発時
![StickC02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/190a44df-66bc-d2c8-1eb7-108a497d91e8.png)

### kintone イベント当日計測（グラフ表示）
![kintone01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/05f130d0-bbb6-5c8c-8843-08346018e271.png)

### シリアルモニタ
![StickC01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/fc32af30-4a68-95bc-504c-22987eb04492.png)

## 問題点

普通なら、温湿度、気圧の計測情など関係なくI2C通信で測定値を取得できるできる筈ですが、最初問題なくても一度バッテリーがなくなるまで実行し、充電後に再開すると、突然温湿度が計測できなくなる場合があります。

実際そのような現象が発生した時にデバッグしてみました。
Wire.endTransmission() を確認すると、"2" が戻り値で、処理が中断していました。
以下のリファレンスから、スレーブを選択するためスレーブアドレスを送信しした際に NACK が返送されてきて、処理が中断したことがわかりました。

Arduino 日本語リファレンス　Wire.endTransmission() 
> 送信結果 (byte)
> 0: 成功
> 1: 送ろうとしたデータが送信バッファのサイズを超えた
> 2: スレーブ・アドレスを送信し、NACKを受信した
> 3: データ・バイトを送信し、NACKを受信した
> 4: その他のエラー

I2C通信ではマスター（M5Stick）がスタートコンディションを生成した後、すぐにスレーブ(ENV DHT12)を選択するためにマスターがスレーブアドレスを送信します。本体はスレーブから「了解」（ACK）が返送されますが、何らかのトラブルで「了解しない」（NACK）が返送され、処理が中断したのが原因でした。

温湿度、気圧の計測を先に行うと、問題は解消したようで、気圧センサのアドレス "0x76" で通信終了のストップコンディションでトラブルがあった可能性がありそうです。
M5Stack では同じロジックでこのエラーは発生しないので、M5StickCとENVセンサの組合せ時には注意が必要です。

``` DHT12.cpp
(前略)
uint8_t DHT12::read()
{
	Wire.beginTransmission(_id);
	Wire.write(0);
        if (Wire.endTransmission()!=0) return 1; // <= I2cスレーブデバイスに対する送信が完了
	Wire.requestFrom(_id, (uint8_t)5);
	for (int i=0;i<5;i++) {
		datos[i]=Wire.read();
	};
	delay(50);
	if (Wire.available()!=0) return 2;
	if (datos[4]!=(datos[0]+datos[1]+datos[2]+datos[3])) return 3;
	return 0;
}

float DHT12::readTemperature(uint8_t scale)
{
	float resultado = 0;
	uint8_t error=read(); 
	if (error!=0) return (float)error/100;  // <= 温度計測のここで return
	if (scale==0) scale=_scale;
	switch(scale) {
		case CELSIUS:
			resultado=(datos[2]+(float)datos[3]/10);
			break;
		case FAHRENHEIT:
			resultado=((datos[2]+(float)datos[3]/10)*1.8+32);
			break;
		case KELVIN:
			resultado=(datos[2]+(float)datos[3]/10)+273.15;
			break;
	};
	return resultado;
}
(後略)
```

## 参考情報

M5StickCで小型環境センサ端末を作る
https://ambidata.io/samples/m5stack/m5sitckc/

M5StickCのバッテリー管理AXP192を図にまとめる
https://lang-ship.com/blog/?p=546

ESP8266でHTTP GETするならWiFiClientじゃなくてHTTPClientのほうが良い
https://relativelayout.hatenablog.com/entry/2016/12/21/234344

M5StickC非公式日本語リファレンス HTTPClient
https://lang-ship.com/reference/unofficial/M5StickC/Class/ESP32/HTTPClient/

M5StickC非公式日本語リファレンス WiFiClient
https://lang-ship.com/reference/unofficial/M5StickC/Class/Arduino/WiFiClient/

Arduino 日本語リファレンス Wire.endTransmission()
http://www.musashinodenpa.com/arduino/ref/index.php?f=1&pos=394

ツール・ラボ 第38回 I2C通信手順
https://tool-lab.com/make/pic-practice-38/

I2C通信の使い方
http://www.picfun.com/f1/f06.html