カワリモノ息子の技術メモ的な~

カワリモノ息子とその母の技術メモ的な〜

学校が苦手な息子くんの作品とその母の作品、はたまた日常などいろいろを在宅エンジニア母が綴る

M5StickCとGoogleスプレッドシートで服薬管理~その2:前回服薬日時を取得する~

M5StickCとGoogleスプレッドシートで服薬管理

siroitori.hatenablog.com

前回は、おくすりのんだときにM5StickCのAボタン(表にあるボタン)を押したらGoogleスプレッドシートに日時を書き込むというものでした。

前回服薬した日時が知りたい

次にやりたいことは「前回おくすり飲んだ日時が知りたい」です。
私の薬はだいたい12時間後に飲むのが良いので、よく「前回はいつ飲んだかな…」って思うことが多いです。

知る方法としてスマートスピーカー(Google Home)に聞いたら教えてくれるというのがいいなーとは思いましたがそれはちょっと置いといて。

まずは、M5StickCのBボタン(側面のボタン)を押したらGoogleスプレッドシートから前回の日時を取得してM5StickCのディスプレイに表示させてみようと思いました。

ここでハマったことが多かったのでここによーくメモしておこうと思います。

Googleスプレッドシート側のGASプログラムを修正

スプレッドシートに書き込むプログラムはできてますが、今回はそこから取得してもどすプログラムです。

息子先生から「ぼく戻すプログラム書いてるから見ていいよ~」と言われたので参考にさせていただき作成しました^^;;
息子くんのプログラムはHttp GetのコードでしたがPostでも同じようにいけそうでした。returnで戻すだけ。簡単~と思いながら、下記のようにテスト的に実装しました。

  // 戻り値を設定
  var output = ContentService.createTextOutput();
  output.setMimeType(ContentService.MimeType.JSON);
  output.setContent(JSON.stringify({ result : "success"}));  // とりあえず固定のJSONを戻してみる

  return output;

そしてコードをCtrl+Sで保存。

ちなみに前回書き忘れましたがこのGoogleスプレッドシートに書けるコードはGoogle Apps Script、通称GASって言うそうです。

はまったところその1:GASの更新

M5StackのArduinoコードにどうやって戻り値が戻るかわからないものの、とりあえずこのまま実行してみました。

あれ?なにも変わらない・・・!?

ここでかなり時間を使ってしまいましたが、実はGASが更新できていなかったというオチ。

f:id:toriko0413:20200415125807p:plain

GASを修正したときは、再度「ウェブアプリケーションとして導入…」でデプロイし直しが必要で、なおかつ project version を New にして新しい数字を採番する必要があります!!

息子くん「あーそれそれ。ぼくもちょっとハマったんだよね~」って。もー!おしえてよーーー!

M5StickC側のArduinoプログラム

GASが正常に更新されたら、今度はM5StickCのArduinoプログラムで、HTTPステータスコード200(HTTP_CODE_OK)ではなくコード302が戻るようになってしまいました!
以下で取得されるhttpCodeのことです。

int httpCode = http.POST(json);

コード302(定数はHTTP_CODE_FOUND)は、リダイレクトをあらわしています。
なぜにリダイレクト・・・???

curlで確認してみた

息子くんに聞いたところ「そうそう、GASの戻りはリダイレクトで取得できる仕様なんよ。まずはcurlで試してみたらいいよ」って。

言われるがままWindowsのコマンドプロンプトでcurlコマンドをたたいてみます。

とその前に、簡単にcurlで叩けるように引数なしで動くように引数のeを使っている部分のGASコードをコメントアウトして再デプロイしてから実行しました。

curl -X POST -d "" -verbose "https://script.google.com/macros/~"

スプレッドシートのURLにPOSTを送信すると…

f:id:toriko0413:20200415133918p:plain

これを見たところたしかにリダイレクトしているようです。

次に、-L オプションでcurlを実行するとリダイレクトも追えるようなので実行。

curl -L POST -d "" -verbose "https://script.google.com/macros/~"

f:id:toriko0413:20200415134913p:plain

期待する{ result : "success"} が戻ってきました!

ということは、GASのほうは問題なく動作しているということがわかりました。
Arduinoの書き方を変えないといけないな。

はまったところその2:Arduinoコードでリダイレクトの取得

リダイレクトの取得をArduinoでどう実装するか、ここも時間かかりました。

【2020/06/07 追記】

HTTPSRedirect っていうライブラリを使えばわざわざ長たらしく書かずにサクッと実装できると知りました!下記リンクのブログ参照☆
hack-tnr.hatenablog.com

まずはPostして302(HTTP_CODE_FOUND)で戻った後にリダイレクトする先のURLの取得。

Postの戻りから取得しないといけないのですが、これについては息子くんから「HTTPヘッダのLocationからとるのが良い」と言われたもののLocationがとれない。。

息子くんに見てもらったところ、「HTTPヘッダのLocationがとれるようになってない」といわれコードを追加。
(Serial.println(http.header("Allow"));で確認してLocationが無いとわかったようです。さすが。)

HTTPヘッダのLocationが取得できるようにするために、具体的には、Post発行前に次のコードを追加しました。

  const char* headerNames[] = { "Location"};
  http.collectHeaders(headerNames, sizeof(headerNames)/sizeof(headerNames[0]));

これで、Postしたあとにhttp.header("Location")とすることで、リダイレクト先のURLが取得できるようになりました!

次に、リダイレクト先へのHTTPリクエストの仕方。
いろいろやってみた結果、リダイレクト先へはPostでなくGetすれば良いことがわかりました。やったー。

f:id:toriko0413:20200415150941j:plain

ソース公開

GAS

function doPost(e) {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('シート1');
  var params = JSON.parse(e.postData.getDataAsString());
  var id = params.id;

  if (id == "rec"){
    // 記録モード(Aボタン押下)時のみデータをシートに追加
    sheet.insertRows(2,1);
    sheet.getRange(2, 1).setValue(new Date());     // 受信日時を記録
  }
  
  // 最新行の日時セル内容を取得してフォーマット
  var targetDate = sheet.getRange(2,1).getValue();
  var dateString = "";
  var timeString = "";
  if (targetDate != null){
    dateString = Utilities.formatDate(targetDate,"JST","yyyy/MM/dd");
    timeString = Utilities.formatDate(targetDate,"JST","HH:mm:ss");
  }  
  
  // 戻り値を設定
  var output = ContentService.createTextOutput();
  output.setMimeType(ContentService.MimeType.JSON);
  output.setContent(JSON.stringify({ previnfo_date : dateString , previnfo_time : timeString}));  // 最新行の日時セル内容を取得して戻す

  return output;
}

M5StackのAボタン押下時に"rec"を送って記録させるように修正しました。

また、戻り値に最新行の日時セル内容を取得して戻すようにしました。

Arduino

#include <M5StickC.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

const char* ssid = "ssid";                       // 接続先のSSIDを設定
const char* password = "password";    // 接続先のパスワードを設定
const char* published_url = "https://script.google.com/macros/~~~~";   // GoogleスプレッドシートのデプロイされたURLを設定

char json[100];           // postするjson
String responseString;    // Http Getで戻った値
DynamicJsonDocument json_response(255);   

void setup() {
  M5.begin();
  M5.Axp.ScreenBreath(10);    // 画面の輝度を少し下げる
  M5.Lcd.setRotation(3);      // 左を上にする
  M5.Lcd.setTextSize(2);      // 文字サイズを2にする
  M5.Lcd.fillScreen(BLACK);   // 背景を黒にする

  WiFi.begin(ssid, password);  //  Wi-Fi接続  (-Aは繋がらなかった)
  while (WiFi.status() != WL_CONNECTED) {  //  Wi-Fi AP接続待ち
      delay(500);
      Serial.print(".");
  }
  Serial.print("WiFi connected\r\nIP address: ");
  Serial.println(WiFi.localIP());
}

boolean getRequest(String url){
  //HTTPClient code start
  HTTPClient http;
  
  boolean isSuccess = false;
  
  // configure traged server and url
  http.begin(url); //HTTP

  Serial.print("[HTTP GET] begin...\n");
  // start connection and send HTTP header
  int httpCode = http.GET();
  
  // httpCode will be negative on error
  if (httpCode > 0) {
    // HTTP header has been send and Server response header has been handled
    Serial.printf("[HTTP GET] Return... code: %d\n", httpCode);
      
    // file found at server
    if (httpCode == HTTP_CODE_OK) {
      // 200
      Serial.println("[HTTP GET] Success!!");
      responseString = http.getString();
      Serial.println(responseString);

      isSuccess = true;       
    }
  } else {
    Serial.printf("[HTTP POST] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
  }
  http.end();
  
  return isSuccess;  
}

boolean postRequest(String url, String json){
  //HTTPClient code start
  HTTPClient http;
  
  boolean isSuccess = false;
  Serial.println(json);

  Serial.print("[HTTP POST] begin...\n");
  // configure traged server and url
  http.begin(url); //HTTP

  // Locationをとるためにこれを書かないといけない
  const char* headerNames[] = { "Location"};
  http.collectHeaders(headerNames, sizeof(headerNames)/sizeof(headerNames[0]));
 
  Serial.print("[HTTP POST] ...\n");
  // start connection and send HTTP header
  int httpCode = http.POST(json);
 
  // httpCode will be negative on error
  if (httpCode > 0) {
    // HTTP header has been send and Server response header has been handled
    Serial.printf("[HTTP POST] Return... code: %d\n", httpCode);
 
//    Serial.println("Allow");
//    Serial.println(http.header("Allow"));
      
    // file found at server
    if (httpCode == HTTP_CODE_OK) {
      // 200
      Serial.println("[HTTP] Success!!");
      String payload = http.getString();
      Serial.println(payload);

      isSuccess = true;HTTP_CODE_OK
      
    }else if (httpCode == HTTP_CODE_FOUND) {    
      // 302 … ページからreturnが戻った場合はリダイレクトとなりこのエラーコードとなる
      String payload = http.getString();
      Serial.println(payload);

      // ヘッダのLocation(リダイレクト先URL)を取り出す
      Serial.println("Location");
      Serial.println(http.header("Location"));

      // リダイレクト先にGetリクエスト
      isSuccess = getRequest(http.header("Location"));
    }
  } else {
    Serial.printf("[HTTP POST] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
  }
  http.end();
  
  return isSuccess;
}

void loop() {
  M5.update();
  
  if (M5.BtnA.wasPressed()){
    // お薬を飲んだ情報を送る

    M5.Lcd.setCursor(0, 0, 1);
    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setTextColor(WHITE);
    M5.Lcd.println("Sending...");
    
    //make JSON
    sprintf(json, "{\"id\": \"%s\" , \"temp\": \"%s\" }", "rec", "temp value");   // Recordモード
    boolean isSuccess = postRequest(published_url ,json);
    
    M5.Lcd.setCursor(0, 0, 1);
    M5.Lcd.fillScreen(BLACK);
    
    if (isSuccess){
      // 成功時のM5画面表示
      M5.Lcd.setTextColor(WHITE);
      M5.Lcd.println("Recorded !!");
      M5.Lcd.println("");

      // 戻ってきたjsonを処理
      deserializeJson(json_response, responseString.c_str());
      const char* prevDate = json_response["previnfo_date"];
      const char* prevTime = json_response["previnfo_time"];
      M5.Lcd.println(prevDate);
      M5.Lcd.println(prevTime);
      
    }else{
      // 失敗時のM5画面表示
      M5.Lcd.setTextColor(RED);
      M5.Lcd.println("  !!!!!!!!!");
      M5.Lcd.println("REPORT FAILED");
      M5.Lcd.println("Please Retry");
      M5.Lcd.println("  !!!!!!!!!");
    }
  }else if (M5.BtnB.wasPressed()){
    // 前回の飲んだ情報を取得する

    M5.Lcd.setCursor(0, 0, 1);
    M5.Lcd.fillScreen(BLUE);
    M5.Lcd.setTextColor(WHITE);
    M5.Lcd.println("Inquiry...");
    
    //make JSON
    char json[100];
    sprintf(json, "{\"id\": \"%s\" , \"temp\": \"%s\" }", "inquery", "temp value");      // Inqueryモード
    boolean isSuccess = postRequest(published_url ,json);     
    
    M5.Lcd.setCursor(0, 0, 1);
    M5.Lcd.fillScreen(BLUE);
    
    if (isSuccess){
      // 成功時のM5画面表示
      M5.Lcd.setTextColor(WHITE);

      // 戻ってきたjsonを処理
      deserializeJson(json_response, responseString.c_str());
      const char* prevDate = json_response["previnfo_date"];
      const char* prevTime = json_response["previnfo_time"];
      M5.Lcd.println("previous date is");
      M5.Lcd.println("");
      M5.Lcd.println(prevDate);
      M5.Lcd.println(prevTime);

    }else{
      // 失敗時のM5画面表示
      M5.Lcd.setTextColor(RED);
      M5.Lcd.println("  !!!!!!!!!");
      M5.Lcd.println("REPORT FAILED");
      M5.Lcd.println("Please Retry");
      M5.Lcd.println("  !!!!!!!!!");
    }
  }
}

今回、Jsonを扱うためにArduinoJsonっていうライブラリをライブラリマネージャから追加しています。

なんかあまり納得のいくコードの書き方じゃないですけどね…。私がレビュアーならつっこむ。

完成品

M5StickCの
Aボタン(表面のボタン)・・・服薬時刻を記録
Bボタン(側面のボタン)・・・前回の時刻を読み込み
↓ の動画では、Bボタン・Aボタンの順に実行しています。

Aボタンで記録の時も記録した日時をディスプレイ表示するようにしました。
Bボタンで前回の日時を表示時は画面背景色を青にしました。

さいごに

あとはスマートスピーカーと連携した改良をすすめたいなと思っています。
しかしブログ書いてたらそれだけで1日かかってしまったりするのでなかなかスピードは出ませんが…。

Twitterのみなさんがいろいろ声かけてくださったり教えてくださったりするので嬉しいです^^

そして、息子くんの知識に母ながら感服しているであります。ぐぬぬ。