プログラミング素人のはてなブログ

プログラミングも電気回路も専門外の技術屋の末端が勉強したことや作品をアウトプットするブログ。コードに間違いなど見つけられたら、気軽にコメントください。 C#、Python3、ラズパイなど。

M5Stackでお薬飲んだ記録を作る

お薬飲んだ記録をするものを作りたい

M5Stackを入手したので、画面とボタンが付いているメリットを最大に活かせる投薬管理アプリを作りました。

しろいとり子さんのブログを大いに参考にさせていただきました。
siroitori.hatenablog.com
siroitori.hatenablog.com

VS codeのPlatformIOを使う

ESP32のときも感じましたが、M5StackをArduino IDEコンパイル・書き込みをするととても遅いです。
ぐぐるVS codeのPlatformIOという拡張を使うと速い、という情報を見つけたのでこれをインストールします。

↓を参考にVS codeにPlatformIOという拡張をインストールします。
qiita.com
f:id:s51517765:20210504202520j:plain

シリアルモニタが文字化けするのでPlatformIOのmonitor_speedを変更します。
qiita.com
プロジェクト内のplatform.iniで以下のようにmonitor_speedを追記します

monitor_speed = 115200

変更内容やコードの規模にもよると思いますが、ほぼほぼ40~60秒程度で書き込みまでできているようです。
Arduino IDEでは数分かかることもあることを考えると、ESP32やM5Stackでは完全にこちらに移行しようと思います。

しかしながら、PlatformIOにはBtnAが誤作動するBugがあるようです。

github.com
Issueに上がっているので、いずれ直ることを期待しますがvoid setup()内でWiFi.setSleep(false);することで回避できました。

I am using ESP32 library v. 1.0.6 and the "black" M5Stack with the same issue.
Even if I did not noticed the issue on the M5ez demos, I started to have it recently when I decided to start working on the M5 and reinstalled the libraries. Probably I got the new one.
I have added the
WiFi.setSleep(false);
in the Setup() sequence just after my Wire.Begin(); instructions, and it seems to work.
The battery consumption is not an issue for me, since my final device will have to work always connected to the power, and so I will remove the battery at the end of the development prior to install.

PaltformIOではプロジェクトごとにInitializeが必要です。初回はその他インストールが走るのかこれには数分かかります。
f:id:s51517765:20210504202556j:plain
M5Srack Core ESP32、Arduinoを選択しプロジェクトを作成します。(Board選択でM5と入力するとジャンプします)
f:id:s51517765:20210504202608j:plain

コンパイル・ビルドは画面下のチェック、Serial Monitorはプラグ?のアイコンです。
f:id:s51517765:20210504203412p:plain

実装していく

基本的にはしろいとり子さんのブログを踏襲していきますが以下の改良を行いました。
・ボタンをM5Stack Basicで3つ使えるようにする
・前回飲んでからの時間を表示できるようにする
・現在時刻を表示する
・バッテリー残量を表示する
・自分好みにリファクタリング

◆画面の回転
M5Stack Basicはボタンが3つあるのでこれを活用したいのですが、このボタンにそれぞれお薬を対応させます。
このため、デフォルトの向きでは都合が悪いので、90°回転させます。

void setup()内でLCDの向きを指定します。引数が0, 1, 2, 3, 4 …で90°ごとに回転し、4からはミラー反転します。
ここで、デフォルトが1の90°回転した状態のようで、欲しい角度は0でした。

M5.Lcd.setRotation(0); //90°回転

◆GAS(Google app script)とGoogle Spreadsheetとの連携
スプレッドシートを使うときは、どこかで↓のような権限を求める画面が出てくるはずなのですが、なぜか出てこなくて迷いました。
f:id:s51517765:20210504205205j:plain
デプロイの管理画面から権限を指定してデプロイしなおすことで権限設定ができました。
f:id:s51517765:20210504205152j:plain

スプレッドシートにデータを投げたり取得したりするには、返り値のあるPostで実行します。

GASではどのボタンが押されたかを引数で取得して、お薬ごとに列を分けて日時を書き込み最後の服薬日時と経過時間を算出してJSON形式でreturnします。
M5Stackの電源オンのときは、前回の時刻と経過時間だけが欲しいので、引数を0~2以外(実装では-1)をいれると、書き込みをせずにreturnのみします。

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

if(id=="0"){
  // データをシートに追加
    var range = sheet.getRange("A2");
  range.insertCells(SpreadsheetApp.Dimension.ROWS);
  sheet.getRange(2, 1).setValue(new Date());     // 受信日時を記録
}
else if(id=="1"){
       var range = sheet.getRange("B2");
  range.insertCells(SpreadsheetApp.Dimension.ROWS);
  sheet.getRange(2, 2).setValue(new Date());     // 受信日時を記録
}
else if(id=="2"){
      var range = sheet.getRange("C2");
  range.insertCells(SpreadsheetApp.Dimension.ROWS);
  sheet.getRange(2, 3).setValue(new Date());     // 受信日時を記録
}
  // 最新行の日時セル内容を取得してフォーマット
  var targetDate0 = sheet.getRange(2,1).getValue();
  var dateString0 = "";
  if (targetDate0 != null){
    dateString0 = Utilities.formatDate(targetDate0,"JST","MM/dd HH:mm");
  }  
  var targetDate1 = sheet.getRange(2,2).getValue();
  var dateString1 = "";
  if (targetDate1 != null){
    dateString1 = Utilities.formatDate(targetDate1,"JST","MM/dd HH:mm");
  }  
  var targetDate2 = sheet.getRange(2,3).getValue();
  var dateString2 = "";
  if (targetDate2 != null){
    dateString2 = Utilities.formatDate(targetDate2,"JST","MM/dd HH:mm");
  }  
  
  // レスポンス
  var response = {
    data: { "0": dateString0 ,"1":dateString1,"2":dateString2},
    past:{ "0": get_history(0) ,"1":get_history(1),"2":get_history(2)}
  };
  return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON);
}

function get_history(num){
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('シート1');
  var range;
  if(num==0)  range = sheet.getRange("A2");
  else if(num==1)  range = sheet.getRange("B2");
  else if(num==2)  range = sheet.getRange("C2");

  var value = range.getValue();
  var lasttime =Utilities.formatDate(value,"JST","MM/dd HH:mm")
  var timeDelta = new Date()-value;
  Logger.log(lasttime);
  var h =parseInt(timeDelta/1000/60/60);
  var d = parseInt(h/24);
  var res;
  if(d>0) res = lasttime.toString() + "("+d.toString()+"days before)";
  else res = lasttime.toString() + "("+h.toString()+"h before)";
  Logger.log(res);

  return res;
}

GASの動作確認は以下のようなcurlコマンドをコマンドプロンプトで実行します。
このようにすると、GASのdoPost(e)が呼ばれるようです。

curl -H "Content-Type: application/json" -d '{"Alice":"1", "Bob":"2" }' -L "https://script.google.com/macros/s/****/exec

なのですが、思ったように返ってこなくて悩んでいましたがdoPost(e)がBugっていたようです。
例外が発生したときに、GASは何も発さずに止まってしまうのでわかりませんでした。
Try~Catchとかを入れておくか、事前に単体テストで確認しておくべきでした。

前回飲んでからの時間の計算はM5Stack側でやるかGAS側でやるか両方可能ですがここではGASで行うことにしました。

◆現在時刻の表示
あったら便利ぐらいの位置づけです。
ntpサーバーにアクセスして時刻を取得、表示します。

void getTime()
{
  const long gmtOffset_sec = 3600 * 9;     //UTC + 9
  const int daylightOffset_sec = 3600 * 0; //Summer Timeなし
  const char *ntpServer = "pool.ntp.org";
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo))
  {
    Serial.println("Failed to obtain time");
    return;
  }
  Serial.println(&timeinfo, "%m %d %A %Y %H:%M:%S");
  M5.Lcd.setCursor(0, LineProgress);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setTextColor(CYAN);
  M5.Lcd.print(&timeinfo, "%m/%d %a %H:%M");
  M5.Lcd.setTextSize(3);
  return;
}

◆バッテリー残量の表示
M5Stackはバッテリー駆動が出来るので残量表示はあったほうがいいと思い探してみると、残量をパーセントで出力する機能があります。
これをスマホなどのバッテリー残量表示のようにゲージを#####のように文字列で作成し表示するようにしました。

void showBattery()
{
  int BatteryLv = M5.Power.getBatteryLevel();

  if (BatteryLv < 40)
    M5.Lcd.setTextColor(RED);
  else
    M5.Lcd.setTextColor(GREEN);
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.print(BatteryLv);
  M5.Lcd.print("[");
  for (int i = 0; i < 5; i++)
  {
    if (i * 20 < BatteryLv)
      M5.Lcd.print("#");
    else
      M5.Lcd.print(" ");
  }
  M5.Lcd.println("]");
}

しかし、M5Stack basicのバッテリー容量は標準で110mAhで一方、消費電力が200mAほどあるらしいので、せいぜい30分しか持ちません。
なので実質的にはバッテリー駆動は現実的ではない、と思っています。
USB電源をつないで使用するならバッテリー残量は意味がないですが、モバイルバッテリーで駆動するなら少し意味があるかな、と思っています。

認証情報は別ファイルに記載する

GitHubでソースは公開したいので、認証情報などは.gitignoreしたいです。
このため、auth.hというファイルを作成し、認証情報はこの中に書き込みます。

extern char *ssid = "**********";
extern char *password = "**********";
// GoogleスプレッドシートのデプロイされたウェブアプリのURLを設定
extern char *published_url = "https://script.google.com/macros/s/********/exec";

メインのコードでは↓のようにして読み込ませると、auth.hの中で宣言した変数がグローバル変数として使えるようになります。

#include "auth.h"

これが最適な手法かどうかはまだよくわかっていません。

まとめ

お薬を飲んだ記録をするアプリをつくりました。
実際に活用できるものを作るのは楽しいですね。
f:id:s51517765:20210504221856p:plain

github.com