非接触温度センサで体温推定(m5stack利用)

# はじめに

前回([非接触温度センサで体温計測](https://qiita.com/kazu1017/items/549067cbd5d29f183b06))は、arduino unoを使って非接触体温計の作成を試みましたが、測定結果の蓄積機能がなく今ひとつでした。今回は、[m5stack](https://www.switch-science.com/catalog/3647/)を使って、Wi-Fi経由でクラウドへのデータ蓄積が可能な非接触体温計を作ってみます。

# 動作の概要

![20200413.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/614257/898395af-657e-5901-c6d6-f99f4991b90b.jpeg)

- 測定について
    - 温度センサーにおでこをかざして体温を計測します。
    - 測定距離まで近づいたことを超音波センサーで検知してから計測します。
    - 平熱であればピピッ、発熱ありであればブーブー、と鳴り、ディスプレイに測定結果を表示します。
    - おでこが遠すぎる場合は「ブブブブブ」と鳴り、もう少し近づくように促します。
    - 測定できるのはおでこの表面温度なので、測定結果を補正して体温の推定値を算出します。

- 測定結果の蓄積について
    - 測定結果はWi-fiを使ってJSONでアップロードします。
    - Google SpreadsheetとGoogle App Scriptで記録します。

# 準備するもの

- M5stack(https://www.switch-science.com/catalog/3647/)
- 非接触温度センサー MLX90614 (https://www.sengoku.co.jp/mod/sgk_cart/detail.php?code=EEHD-4EZP)
- 超音波センサー(http://akizukidenshi.com/catalog/g/gM-11009/)
- 10kΩ抵抗×2個
- ジャンパーワイヤ
- ブレッドボード(http://akizukidenshi.com/catalog/g/gP-05156/)
- 適当なレゴ（ケース用）

# 回路図

![回路図_20200417.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/614257/3ee18036-7923-021f-8c1d-2fd8391204be.png)

ブレッドボードとジャンパワイヤで頑張って配線します。

# コーディング

- m5stack側のコーディングと、google app script側のコーディングをします。

### m5stack側

- arduino IDEを使います。
- M5stackの初期設定をします。(https://docs.m5stack.com/#/en/arduino/arduino_development)
- ライブラリをいくつかダウンロードします。(Adafruit MLX90614、ArduinoJson)
- SSIDとpasswordはWi-Fi環境に合わせて入力します。
- hostには、google app scriptのURL「Current web app URL:」を入力します(後述)。
- SDカード(microSDHC)にフォントとバックグラウンド画像を入れておきます。
- バックグラウンド画像はいらすとやさんのイラストを使わせていただき、下記のように作成しました。フォントは[M5Stack の LCD に TFT_eSPI を使って日本語フォント "源真ゴシック" で表示する](https://qiita.com/ma2shita/items/fef1608fc9cf3a7bd46a)を参考にさせていただきました。

![bg01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/614257/377394f2-eb8d-374d-ed28-4d389ca916a0.jpeg)

```cpp:mlx90614_test.ino 
#include <M5Stack.h>
#include <Adafruit_MLX90614.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

// Wi-Fi設定
WiFiClient client;
const char* ssid = "******";
const char* password = "******";

// GAS設定
const char* host = "https://script.google.com/macros/s/******/exec";

// 温度センサー初期設定
#define TEMP_THRESHOLD 37.0
Adafruit_MLX90614 mlx = Adafruit_MLX90614();

// 体温推定パラメータ
// T_est = T_raw - HOSEI_A * distance - HOSEI_B 
#define HOSEI_A -0.7880
#define HOSEI_B -0.2873

//超音波距離センサ定数設定
#define TrigPin 5 // Trig Pin定義
#define EchoPin 2 // Echo Pin定義
#define OK_DISTANCE 7.0 // 測定は7cm以内

// スピーカー定数設定
#define SPEAKER 12 //スピーカーの出力ピン番号
#define OK_SOUND 2000.0 // 2000Hz
#define NG_SOUND 300.0 // 300Hz
#define LONG 250 // 250msec
#define SHORT 50 // 50msec
#define VOLUME 1

// 測定試行回数・間隔
#define TRY_MAX 5
#define TRY_INTERVAL 250
int too_far;

// ArduinoJson設定
const int capacity = JSON_ARRAY_SIZE(TRY_MAX) + (TRY_MAX + 2)*JSON_OBJECT_SIZE(2);
char buffer[1024];

// 日本語フォント設定
const char* f24 = "genshin-regular-24pt"; // without Ext
#define FSIZE 24

// the setup routine runs once when M5Stack starts up
void setup() {

  // 各種初期化
  M5.begin();
  mlx.begin();
  Serial.begin(9600); //M5.begin()の後にSerialの初期化をしている
  M5.Power.begin();

  // Lcd設定
  M5.Lcd.setRotation(2); // 縦表示（usb端子が下）
  M5.Lcd.loadFont(f24, SD); // SDカードからフォント読み込み
  
  // 超音波センサ設定
  pinMode( TrigPin, OUTPUT ); //デジタル入出力のTrigpinをOUTPUTに指定
  pinMode( EchoPin, INPUT ); //デジタル入出力のEchopinをINPUTに指定
  dacWrite(25,0);  //ノイズ対策

  // スピーカー設定
  ledcSetup(TONE_PIN_CHANNEL, 0, 10);
  ledcAttachPin(SPEAKER_PIN, TONE_PIN_CHANNEL);

  // Wifi接続
  WiFi.begin(ssid, password);  //  Wi-Fi APに接続
  M5.Lcd.print("Wifi APに接続しています");
  while (WiFi.status() != WL_CONNECTED) {  //  Wi-Fi AP接続待ち
    M5.Lcd.print(".");
    delay(100);
  }
  M5.Lcd.println(" WiFi APに接続しました。");
  M5.Lcd.print("IP address: ");
  M5.Lcd.println(WiFi.localIP());

  // Lcd表示
  M5.Lcd.drawJpgFile(SD, "/bg01.jpg");
  display_text();
  too_far = 0;

}

// the loop routine runs over and over again forever
void loop(){
  float temp[TRY_MAX];
  float distance[TRY_MAX];
  float temp_avg;
  float distance_avg;
  int n = 0;
  int max = TRY_MAX;

  if(measure_distance() < OK_DISTANCE){

    // 表面温度測定（試行回数分）
    read_temp_sensor(temp, distance);
    temp_avg = 0.0;
    distance_avg = 0.0;
    for(int i=0; i< TRY_MAX; i++){
      if(distance[i] < OK_DISTANCE){
        temp_avg += temp[i];
        distance_avg += distance[i];
      }else{
        max = max - 1;
      }
    }

    // 体温推定
    distance_avg = distance_avg / max;
    temp_avg = temp_avg / max - (distance_avg * HOSEI_A) - HOSEI_B;

    // 結果判定
    if(temp_avg < TEMP_THRESHOLD){

      // 平熱
      play_sound(OK_SOUND, SHORT, 2);
      M5.Lcd.fillRect(FSIZE, FSIZE-3, FSIZE*8, FSIZE*5+3, WHITE);  
      M5.Lcd.setTextColor(BLUE, WHITE);
      M5.Lcd.setCursor(1*FSIZE, 1*FSIZE); M5.Lcd.print("あなたの体温は");
      M5.Lcd.setCursor(1*FSIZE, 2*FSIZE); M5.Lcd.printf("%2.1f℃　です。", temp_avg);
      M5.Lcd.setCursor(1*FSIZE, 3*FSIZE); M5.Lcd.println("平熱ですね！");
      M5.Lcd.setCursor(1*FSIZE, 4*FSIZE); M5.Lcd.println("今日もがんばりま");
      M5.Lcd.setCursor(1*FSIZE, 5*FSIZE); M5.Lcd.println("しょう！");
    }else{

      // 発熱
      play_sound(NG_SOUND, LONG, 2);
      M5.Lcd.fillRect(FSIZE, FSIZE-3, FSIZE*8, FSIZE*5+3, WHITE);  
      M5.Lcd.setTextColor(RED, WHITE);
      M5.Lcd.setCursor(1*FSIZE, 1*FSIZE); M5.Lcd.print("あなたの体温は");
      M5.Lcd.setCursor(1*FSIZE, 2*FSIZE); M5.Lcd.printf("%2.1f℃　です。", temp_avg);
      M5.Lcd.setCursor(1*FSIZE, 3*FSIZE); M5.Lcd.println("熱があるかもしれ");
      M5.Lcd.setCursor(1*FSIZE, 4*FSIZE); M5.Lcd.println("ません。体温計で");
      M5.Lcd.setCursor(1*FSIZE, 5*FSIZE); M5.Lcd.println("測定して下さい！");
    }

    // 結果をGCPにアップロード
    //M5.Lcd.unloadFont();
    postValues(temp_avg, distance_avg, temp, distance);
    //M5.Lcd.loadFont(f24, SD);
    display_text();

    // 測定が正常完了→カウンタリセット
    too_far = 0;

  // 温度が高いのに距離が遠い場合→測定可能距離まで近づくよう促す
  }else if(mlx.readObjectTempC() > 28.0){

    // 3回以上のカウントで発動
    too_far++;
    if(too_far > 3){
      play_sound(NG_SOUND, SHORT, 3);
      M5.Lcd.fillRect(FSIZE, FSIZE-3, FSIZE*8, FSIZE*5+3, WHITE);  
      M5.Lcd.setTextColor(BLACK, WHITE);
      M5.Lcd.setCursor(1*FSIZE, 1*FSIZE); M5.Lcd.print("少し遠いようです");
      M5.Lcd.setCursor(1*FSIZE, 2*FSIZE); M5.Lcd.print("もう少し近づいて");
      M5.Lcd.setCursor(1*FSIZE, 3*FSIZE); M5.Lcd.println("みてください。");
      delay(1500);
      display_text();
      too_far = 0;
    }
  }else{
    // 温度が低くなった→カウンタリセット
    too_far = 0;
  }
  delay(500);
}

// 結果アップロード
void postValues(float temp_avg, float distance_avg, float temp[], float distance[]) {

  // Jsonオブジェクト初期化
  DynamicJsonDocument doc(capacity);

  // 平均値を格納
  doc["average"]["temp"] = temp_avg;
  doc["average"]["distance"] = distance_avg;
  
  // 生値を格納（配列）
  for(int i=0; i< TRY_MAX; i++){
    JsonObject obj1 = doc["raw"].createNestedObject();
    obj1["temp"] = temp[i];
    obj1["distance"] = distance[i];
  }

  // シリアルコンソールに表示（デバッグ用）
  serializeJson(doc, Serial);
  Serial.println("");

  // JSONシリアライズ
  serializeJson(doc, buffer, sizeof(buffer));

  // GCPにHTTP接続開始
  HTTPClient http;
  Serial.println(http.begin(host));

  // POSTメソッドでJSONをアップロード
  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);

  // 結果が200OKの場合
  if( status_code == 200 ){
    Stream* resp = http.getStreamPtr();

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

    serializeJson(json_response, Serial);
    Serial.println("");

  //その他の場合はデバッグログを出力
  }else{
    Serial.println(http.getString());
  }

  // HTTP切断
  http.end();
}

// toneEx
//  引数 
//   frequency (Hz)
//   vol (0 ～ 9、0:無音 9:最大)
void toneEx(uint16_t frequency, uint16_t vol) {
  ledcSetup(TONE_PIN_CHANNEL, frequency, 10);
  ledcWrite(TONE_PIN_CHANNEL,0x1FF>>(9-vol));
}

// NoToneEx
void noToneEx() {
  ledcWriteTone(TONE_PIN_CHANNEL, 0);
  digitalWrite(SPEAKER_PIN, 0);
}

// ブザー音再生
void play_sound(float freq, int beattime, int number){
  for(int i=0; i<number; i++){
    toneEx(freq,VOLUME); // ド
    delay(beattime);
    noToneEx();
    delay(beattime);
  }
}

// LCD表示(デフォルト表示)
void display_text(){
  M5.Lcd.fillRect(FSIZE, FSIZE-3, FSIZE*8, FSIZE*5+3, WHITE);  
  M5.Lcd.setTextColor(BLACK, WHITE);
  M5.Lcd.setCursor(1*FSIZE, 1*FSIZE); M5.Lcd.println("体温を測定します");
  M5.Lcd.setCursor(1*FSIZE, 2*FSIZE); M5.Lcd.println("おでこをセンサー");
  M5.Lcd.setCursor(1*FSIZE, 3*FSIZE); M5.Lcd.println("に近づけてくださ");
  M5.Lcd.setCursor(1*FSIZE, 4*FSIZE); M5.Lcd.println("い。");
}

// 距離測定（超音波センサ使用）
float measure_distance(){
  float ProDelay = 0; //Echo出力のHigh期間を格納する変数
  float Distance = 0; //計算した距離を格納する変数

  digitalWrite(TrigPin, LOW); //10番ピンからLOWを出力
  delayMicroseconds(10); //10μs待機

  //超音波を出力するためのトリガ信号生成
  digitalWrite( TrigPin, HIGH ); //トリガ信号Highパルスエッジ
  delayMicroseconds( 10 ); //トリガ信号パルス幅10μsを生成
  digitalWrite( TrigPin, LOW ); //トリガ信号Lowパルスエッジ

  ProDelay = pulseIn( EchoPin, HIGH ); //11番ピンに入力されるEchoピンのHigh期間を測定
  Distance = 340*ProDelay/2/10000; // 音速340m/sとして距離の計算とcmへの換算
  return Distance;
}

// 表面温度測定（複数回試行）
void read_temp_sensor(float temp[], float distance[]){
  M5.Lcd.fillRect(FSIZE, FSIZE-3, FSIZE*8, FSIZE*5+3, WHITE);  
  M5.Lcd.setTextColor(BLACK, WHITE);
  M5.Lcd.setCursor(1*FSIZE, 1*FSIZE); M5.Lcd.print("測定中です");

  for(int i = 0; i < TRY_MAX; i++){
    temp[i] = mlx.readObjectTempC();
    distance[i] = measure_distance();
    Serial.print("temp, "); Serial.print(temp[i]); Serial.print(", distance, "); Serial.println(distance[i]);
    play_sound(OK_SOUND, SHORT, 1);
    delay(TRY_INTERVAL);
  }
  Serial.println(" ");
  return;
}

```

### google app script側

- google spreadsheetを新規作成します。
- メニューから「ツール」→「スクリプトエディタ」を選択し、スクリプトエディタを開いて、下記スクリプトを入力します。
- \*\*\*\*には、google spreadsheetのidを入力します。(スプレッドシートのURLがhttps://docs.google.com/spreadsheets/d/****/edit#gid=0 だとした場合の****の部分）
- 作成したら、「リソース」→「ウェブアプリケーションとして導入」で公開します。(アクセス権は「Anyone, even anonymous」とします)
- 更新したら版数(Project Version)を「new」にして改版するのを忘れずに！
- scriptのURL「Current web app URL:」をM5stack側のコードに入力します。

```javascript:code.gs
function doPost(e) {
  // パラメータ取り出し
  var params = JSON.parse(e.postData.getDataAsString());

  // スプレッドシート情報収集
  var spreadsheet = SpreadsheetApp.openById("****"),
      sheet = spreadsheet.getSheets()[0],
      date = new Date();

  // A列の最終行を取得
  const columnBVals = sheet.getRange('A:A').getValues();
  var lastrow = columnBVals.filter(String).length;

  // 挿入データ準備
  var data = [];
  for(var i in params.raw){
    data.push([date, 0, 0, params.raw[i].distance, params.raw[i].temp]);
  }
  data.push([date, params.average.distance, params.average.temp, 0, 0]);

  // データ挿入
  sheet.getRange(lastrow+2,1,params.raw.length+1,5).setValues(data);

}

```

# できていないこと

- 朝は低めに、夕方は高めに出てしまいます。これが、おでこの表面温度が朝低く夕方高いのか、体温そのものが朝→夕方で上昇しているのかは不明ですが、いずれにしろあまり正確ではなさそうです。なんとかしなくてはいけません。

![chart2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/614257/c0eae298-d7b0-04d6-8cd1-cd0ad30b6195.png)

# (参考)コード詳細

### LCD表示

[M5StickCのDisplay周り解析](https://lang-ship.com/blog/work/m5stickc-display/)と、[M5Stack の LCD に TFT_eSPI を使って日本語フォント "源真ゴシック" で表示する](https://qiita.com/ma2shita/items/fef1608fc9cf3a7bd46a)を参考にさせていただきました。M5.Lcd.fillRect(FSIZE, FSIZE-3, FSIZE*8, FSIZE*5+3, WHITE)でスクリーンをいったん消去して、M5.Lcd.setCursor(1*FSIZE, 1*FSIZE); M5.Lcd.print("")のように１行ずつ表示しています。

### 距離測定(超音波センサ使用)

[Arduino 入門 Lesson 10 【超音波距離センサ編】](https://omoroya.com/arduino-lesson10/)を参考にさせていただきました。

### 温度測定

[簡易温度計を作る](https://qiita.com/s_fujii/items/1cc5fe6206f6dc24d8cc)を参考にさせていただきました。

### 体温推定

- しばらく使ってみると、おでことセンサーの距離が遠くなるほど測定値が低めに出ることがわかりました。試行データを踏まえて、生データに(おでことセンサーの距離)×0.7880を加算しています。
- おでこの表面温度は低めに出るので、生データにさらに0.2873を加算しています。

![chart.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/614257/1501463d-9a66-fadf-2fa3-31ebdd8ff522.png)

### スピーカー

[Arduinoでパーツやセンサーを使ってみよう～スピーカー編(その1) | Device Plus - デバプラ](https://deviceplus.jp/hobby/entry019/)と、[M5Stackのビープ音、Tone音の爆音対策: 猫にコ・ン・バ・ン・ワ](http://nuneno.cocolog-nifty.com/blog/2020/01/post-b9cd2c.html)を参考にさせていただきました。

### Json生成

[Serialization tutorial | ArduinoJson 6](https://arduinojson.org/v6/doc/serialization/)の「4.2.3 Adding members」と、「4.3.4 Adding nested objects」、「4.4.2 Specifying (or not) the size of the output buffer」を参考にさせていただきました。

### google app scriptへのアップロード

[M5Stick-CでJsonをPOSTする - Qiita](https://qiita.com/poruruba/items/4bf6a52520e431a8f4a5)を参考にさせていただきました。httpclientが結構メモリを食うようで、前後にM5.Lcd.unloadFont()、M5.Lcd.loadFont(f24, SD)を入れて、フォントのメモリをいったん開放しないとうまくいかないかもしれません。

### JSONのパースとspreadsheetへのデータ挿入(google app script側)

[Google Apps Scriptで配列を使ってスプレッドシートにデータ行を追加する方法](Google Apps Scriptで配列を使ってスプレッドシートにデータ行を追加する方法)と、[Google Apps ScriptのdoPostでJSONなパラメータのPOSTリクエストを受ける - Qiita](https://qiita.com/shirakiya/items/db22de49f00710478cfc)を参考にさせていただきました。