2019年10月31日木曜日

実証実験第2クールに向けての準備

実証実験の第2クールは一般の利用者に対して行う。ここで「一般の利用者」とは、本システムの開発関係者ではないということを意味する。
したがって、システムは設定を含めて自動化をしなければならない。トラブルの発生の都度、システムの設定やプログラムのバグ取りをすることはできない。また、システムの起動停止も自動化しなければならない。
ただし、遠隔メンテナンスができるように外部からssh接続ができることが望ましい。

そのために以下の修正を行った。
  1. OS起動とともに発話PUSHプログラム(~/pill-reminder/example.js)を自動起動した。これは/etc/rc.localに起動コマンドを追加することによって実現した。
  2. 外部からsshでサーバにアクセスできるようにした。そのために、サーバにngrokをインストールし、 /etc/rc.localにngrokを使ってsshをトンネリングするコマンドを追記した。これは/etc/rc.localに起動コマンドを追加することによって実現した。
  3. 起動時に発話依頼プログラム(~/pill-reminder/pill-reminder.js)が自動起動するようにした。※注意点としてこれはngrokの前に記述しなければ実行されない。
  4. 発話PUSHプログラム(~/pill-reminder/example.js)が、自身のIPアドレスを自動取得できるようにgetLocalAddress()関数を追加した。
  5. 発話PUSHプログラム(~/pill-reminder/example.js)が、Google HomeのIPアドレスをGoogleスプレッドシートから取得できるようにした。そのためにWebAPI(A2)にdoGetエントリーを付け加え、Googleスプレッドシートから読み込んだGoogle HomeのIPアドレスを返すようにした。なお、Google HomeのIPアドレスは手作業で設定する。
  6. WebAPI(A2)にGoogle HomeのIPアドレスを返す機能を追加した。
  7. 発話PUSHプログラム(example.js)が、ngrokによって払い出されたURLをGoogleスプレッドシートに記入できるようにした。Googleスプレッドシートはpill-reminder.sheetのシート「設定」である。そのためにWebAPI(A2)のdoPostエントリーを修正してngrok_urlパラメタを受け取るとsetNgrokUrl関数を使って受け取ったURLをスプレッドシートに書き込むよう修正した。
  8. WebAPI(A2)では、GOOGLE_HOME_notify関数で使用する発話PUSHプログラムのURL(ngrokによって払い出されたURL)をGoogleスプレッドシートから取得するように修正した。これによって、発話PUSHプログラムを再起動して払い出しURLが変わっても自動追随ができる。
  9. 発話依頼プログラム(~/pill-reminder/pill-reminder.js) が、自身のIPアドレスを自動取得できるようにgetLocalAddress()関数を追加した。
図1.Googleスプレッドシート

2019年10月29日火曜日

10/16 ビデオ撮影

今後の日程について話し合い、発表に向けてスライド作成、デモのビデオ撮影を行った。ビデオ撮影のみで本日は終了したため、来週はスライド制作を行う。

実証実験の被験者が2人増えたため、そちらについても今後テスト実験を行い、評価してもらおうと思う。

2019年10月28日月曜日

実験9日目

LINEに通知が来ない


現象


朝の薬のアナウンスに対して「朝のお薬飲んだよ」と回答し、AIスピーカーからそれに対する応答(おはようございます。お薬手帳に記録しておきます。)があったにもかかわらず、LINEに通知がこない。

調査


ログ(~/pill-reminder/pill-reminder.log)を調べたところ、次のようになっていた。
2019-10-28 05:34:07.037: "朝の食後の薬"の発話時刻になりました(5回目), id=4bq23ffsj7e6t1d9koos3iegml@google.com
2019-10-28 05:34:08.433: 当該服薬スケジュール(id=4bq23ffsj7e6t1d9koos3iegml@google.com):
{
 "drug_notifies": [
  {
   "title": "朝の食後の薬(済)",
   "description": "ディオバン錠20mg 1錠\nメバロチン錠10 1錠",
   "id": "4bq23ffsj7e6t1d9koos3iegml@google.com",
   "location": "自宅",
   "startTime": "2019-10-27T20:30:00.000Z",
   "endTime": "2019-10-27T21:00:00.000Z",
   "color": "9"
  }
 ],
 "num_reminder": "5",
 "taiking_status": "1",
 "status": "0"
}
2019-10-28 05:34:08.433: 済になったか催促上限オーバーになったので催促を終了します。
リスト1.ログ

5回目のアナウンス時点でtaiking_statusが1になっているので、WebAPI (A2)は正常にIFTTTからのWebhooksを受け付けたものと考えられる。うかつにも、GASのデバッグ用ファイル(debug.gdoc)を消してしまったため確証はない。あくまでもtaiking_statusが1に変わっているという状況証拠のみ。

WebAPI (A2)がIFTTTからのWebhooksを正常に受け付けると、必ずLINEへ通知を送るはず。しかし、通知はなかった。

原因


不明。もし、また同じ現象が発生したら、まずdebugの退避をして後で調査できるように取っておくこと。



2019年10月26日土曜日

実験7日目

想定される問題点


  1. TVなど生活音があるなかで正しく利用者がしゃべる声を聞き取ることができるか。
  2. 複数の利用者が1台のAIスピーカーを共用できるか。ボイスマッチを行うことによって発話者の当日のスケジュールを回答させることができるが、これと同じように1台のAIスピーカーで個人ごとの服薬管理を実現できるか。
  3. QRコードがない場合、どうやって処方内容を入力するか。①手入力、②OCR

上記3については
などのOCRソリューションがある。


2019年10月25日金曜日

実験6日目

問題とその解決策


予定を変更する


WebAPI (A2) の修正


実験2日目のブログで「予定を変更する」機能があるといいのにと書いたが、それを実装した。その方法はGoogle Homeから服用のお知らせがあったのち「後でお薬飲む」と伝えると、Googleカレンダーの開始日・終了日を指定した時間(今回は10分間)だけ遅らせるというものである。

修正対象のプログラムは、WebAPI (A2) である。まず、HTTPリクエストのエンドポイントとなる関数 doPost は以下のように修正する。
/**
 * IFTTTからのリクエストに応じてGoogleカレンダーを服用済みに更新する
 * @param  {Object}   e     POSTメソッドで送られてくるデータ
 *  delay: 10 (開始を10分間遅らせる)
 *  timing: [朝|昼|夜|就寝前]
 *
 */
function doPost(e) {
  var params = JSON.parse(e.postData.getDataAsString());  // ※
  
  if (params.timing) {
    //カレンダーのタイトルと色を変える
    var timing = params.timing;  // => [朝|昼|夜|就寝前]が取れる
    NotifyNotTaking(timing);
  } else if (params.delay) {
    //開始時刻を遅らせる
    var delay = parseInt(params.delay);  // => 遅らせる時間[分]
    delayStartTime(delay);
  }
  
  var out = ContentService.createTextOutput();
  
  //Mime TypeをJSONに設定
  out.setMimeType(ContentService.MimeType.JSON);
  
  //JSONテキストをセットする
  result = {
    status: 'OK'
  };
  out.setContent(JSON.stringify(result));
  
  return out;
}
リスト1.WebAPI (A2) のエントリポイント

パラメータにtimingがなくdelayがある場合、リスト2に示す関数delayStartTimeを呼び出す。
/**
 * 現在時刻の予定を指定した時間だけ遅らせる
 * @param  {Integer}   delay     遅らせる時間(分)
 */
function delayStartTime(delay) {
  debug('delayStartTime:delay=' + delay);
  // 当日の服薬予定(未)をすべて取得する
  var events = CalendarApp.getDefaultCalendar().getEventsForDay(
    new Date(),
    {search: '(未)'}
  );  

  // 取得した予定から現在時刻が開始~終了時刻に含まれる予定を探してdelay(分)だけ遅らせる
  var current_time = new Date();
  events.forEach(function(event,i,array){
    var start_time = event.getStartTime();
    var end_time = event.getEndTime();
    if (start_time <= current_time && current_time <= end_time) {
      event.setTime(delayTime(start_time, delay), delayTime(end_time, delay));
    }
  });
}

// 引数に指定した日時target_timeをdelay[分]だけ遅らせる
function delayTime(target_time, delay){
  var delay_time = new Date(target_time.getYear(), target_time.getMonth(), target_time.getDate(), target_time.getHours(), target_time.getMinutes() + delay, target_time.getSeconds() );
  return delay_time;
}
リスト2.予定時刻を遅らせる関数delayStartTime

当日の予定から'(未)'を含む予定を探し、delayだけ開始・終了時刻を遅らせる。

IFTTTの登録


次に、「後でお薬飲む」とスピーカーに向かって話すと「分かりました。予定を10分遅らせます。」と返答して、Webhooks機能を用いて WebAPI (A3) を呼び出すIFTTTのアクティビティを登録する。

トリガーはGoogle Assistantで、下記のような設定にする。
What do you want to say?
 -> 後でお薬飲む

What's another way to say it? (optional)
 -> 後で飲む

And another way? (optional)
 -> 少し待って

What do you want the Assistant to say in response?
 -> 分かりました。予定を10分遅らせます。

Language
 -> Japanese
リスト3.Trigger

続いて、下記はWeb requestの設定である。
URL
 -> https://script.google.com/macros/s/AKfy..............................nIbMw/exec

Method
 -> POST

Content Type (optional)
 -> application/json

Body (optional)
 -> {"delay": 10}
リスト4.Web request

ここで、URLは WebAPI (A3) のURLである。

ベアボーンサーバ(発話依頼プログラム pill-reminder.js)


ベアボーンサーバ上で稼働する発話依頼プログラム pill-reminder.js は、起動時に読み込んだGoogleカレンダーの予定通りに服薬通知を発話するようプログラムされている。
しかし、服用開始時刻を変更できるようにしたため、それに同期して発話の時刻を変更しなければならない。そこで、予定の開始時間になったら呼び出される関数dosing_remindを以下のように修正した。
function dosing_remind(speech, counter, id) {
 counter++;
 putLog('"' + speech + '"の発話時刻になりました(' + counter + '回目), id=' + id);
 if (counter > 10) {
  putLog('催促回数が上限を超えました');
  return;
 }

 // WebAPI A3 をid指定で叩いて服用済みかどうかを確認する
 webclient.get(
  {
   url: data + "?id=" + id + "&num_reminder=" + counter
  }, 
  function (error, response, body) {
   if(!error) {
    var json = JSON.parse(body);
    putLog('当該服薬スケジュール(id=' + id + '):\n' + JSON.stringify(json, null, " "));
    if(json.taiking_status == "0" && !isOver(json)){
     message = speech + 'を飲んでください';
     putLog(message + '(' + counter + '回目)');
     // まだ開始時刻(json.drug_notifies[0].startTime)になっていなければ、開始時刻にタイマーを設定する
     var startTime = new Date(json.drug_notifies[0].startTime);
     var now = new Date();
     var df = startTime - now;
     if (df > 0) {
      putLog('予定が変更されています。この予定は' + parseInt(df / 60000) + '分後に発話されます。');
      setTimeout(dosing_remind, df, speech, 0, id);//繰り返し
     } else {
      // Google homeへ発話依頼(開始時刻を過ぎている場合)
      setTimeout(dosing_remind, C_INTERVAL, speech, counter, id);//繰り返し
      webclient.get({
       url: url,
       qs: {
        text: message,
       }
      },
      function (error, response, body) {
       if(!error) {
        putLog('発話依頼成功:' + body);
       } else {
        putLog('発話依頼失敗:' + error);
       }
      });
     }
    } else {
     putLog('済になったか催促上限オーバーになったので催促を終了します。');
    }

   } else {
    putLog('WebAPI A3 呼び出しに失敗しました:' + error + '(id=' + id + ')');
   }
  }
 );
}
リスト5.予定の開始時間になったら呼び出される関数dosing_remind




2019年10月24日木曜日

実験5日目

問題とその解決策


間違った応答による誤操作


「就寝前の薬を飲んでください」に対して間違って「夜のお薬飲んだよ」と答えてしまい、夜の予定が「(済)」に変わってしまった。これは実験2日目に起きたのと同じインシデント。

対応策


WebAPI (A2)のNotifyNotTaking()関数でカレンダーのタイトルと色を変更するが、ここで現在時刻が開始時刻(start_time)~終了時刻(end_time)の間にあるかどうか調べて、もしなければ時間外である旨応答する。

ここで、GASからどうやって Google Home に喋らせるかが問題になる。
Google Homeにプッシュ通知する製品にGHKitというものがあるらしい。「外部イベントに応じたプッシュ通知で、好みのメッセージをタイミングよくGoogle Homeで音声再生します!」という触れ込みである。
また、GAS、firebase、google-home-notifierを連携させて指定した時刻になるとGoogle Spreadsheetに入力したテキストをGoogle Homeに喋らせるアプリの開発例がある。

我々の開発しているシステムは、firebaseは使っていないが、どちらかというと後者に近い。ベアボーン上で稼働している発話PUSHサーバ(example.js)をngrokでインターネットから利用可能なWebAPIにして、GASから発話依頼するのが最も簡単な方法だろう。
手順はこうである。
  1. WebAPI (A2) はIFTTTからのカレンダー更新要求(Webhooksを使ってWebAPI を叩く)を待ち受ける。
  2. IFTTTからカレンダー更新要求が届いたら、現在時刻と開始時刻(start_time)~終了時刻(end_time)を比較し、時間外であればベアボーン上で稼働している発話PUSHサーバ(example.js)へ発話依頼する。

問題点


ここで問題が発生した。example.js起動時に出力されたngrokが払い出す公開URLの期限が8時間に制限されていることが分かった
これを回避するには、ngrokのサイトからアクセストークンを取得すればよい。そして発話PUSHサーバ(~/pill-reminder/example.js)のHTTPリクエストをListenするプログラムで下記のようにアクセストークンを指定してngrok.connect()関数を実行する。
/**
 * serverPortをlisten
 *
 */
app.listen(serverPort, function () {
  // トークンを設定
  ngrok.authtoken(token);
  // 現状のセッションを一旦終了
  ngrok.kill();
 
  ngrok.connect({authtoken: token, addr: serverPort, region: 'jp'}, function (err, url) { //修正
    console.log('Endpoints:');
    console.log('    http://' + ip + ':' + serverPort + '/google-home-notifier');
    console.log('    ' + url + '/google-home-notifier');
    console.log('GET example:');
    console.log('curl -X GET ' + url + '/google-home-notifier?text=Hello+Google+Home');
    console.log('POST example:');
    console.log('curl -X POST -d "text=Hello Google Home" ' + url + '/google-home-notifier');
  });
})
リスト1.HTTPリクエストのlisten部分


ここで、tokenはngrokのサイトから取得したAuthtokenである。

※リスト1はngrokのconnectメソッドを利用して公開URLを払い出している。これはv2.3.0の使用方法である。
一方,ngrokを非同期で起動して公開URLを払い出すサンプルがネット上にいくつかある。これはv3.2.5の書き方である。それに従ってリスト1を書き換えたものを以下に示す。

app.listen(serverPort, function () {
 putLog('Endpoints:');
 putLog('    http://' + ip + ':' + serverPort + '/google-home-notifier');
   
 connectNgrok(serverPort).then(url => {
  putLog('    ' + url + '/google-home-notifier');
  putLog('GET example:');
  putLog('curl -X GET ' + url + '/google-home-notifier?text=Hello+Google+Home');
  putLog('POST example:');
  putLog('curl -X POST -d "text=Hello Google Home" ' + url + '/google-home-notifier');
 });
})

/** 
 * ngrokを非同期で起動
 */
async function connectNgrok(port) {
 try {
  let url = await ngrok.connect({
   addr: port,
   region: 'jp', 
   configPath: '~/.ngrok2/ngrok.yml',
   authtoken: token
  });
  return url;
 } catch (err) {
  putLog(err.name + ': ' + err.message);
 }
}
リスト1b.

最初、ngrokのurl払い出しがなかなかうまくいかなかった。試行錯誤しているうち、ngrok.connectメソッドのパラメタに
region: 'jp',
configPath: '~/.ngrok2/ngrok.yml',
を追加すると払い出しができるようになった。regionに'jp'(日本)を設定したのが功を奏したのではないだろうか。近くのサーバを使用することでレスポンスが改善され、ngrok.connectが正しく動作したものと思われる。

ここで教訓になったことは、ngrok v2.3.0とv3.5.0では公開URLの払い出し方法が異なるということだ。

次にWebAPI (A2) を以下のように修正する。
/**
 * 「[朝|昼|夜|就寝前]のお薬飲んだよ」に対して服用済みに変えるファンクション
 *
 * @param  {String}   timing     [朝|昼|夜|就寝前]
 *
 */
function NotifyNotTaking(timing) {
  //var timing = e.parameter.timing;
  //「の薬」とつくイベントの取得  
  //var timing =e.parameter.timing;
  debug('NotifyNotTaking: timing=' + timing);
  
  // 当日の予定の中から timing (朝|昼|夜|就寝前)を含む予定を検索
  var events = CalendarApp.getDefaultCalendar().getEventsForDay(
    new Date(),
    {search: timing}
    // {search:'夜の食後の薬'}
  );  

  // 取得した予定からターゲットの予定を探してタイトルと色を変更
  events.forEach(function(event,i,array){ 
    var title = event.getTitle();
    debug('NotifyNotTaking: title=' + title); 
    if(title.match(/^.の食.の薬\(.+\)$/) || title.match(/就寝前の薬\(.+\)$/)) {
      // 現在時刻と開始時刻(start_time)~終了時刻(end_time)を比較し、時間内であればタイトルを(済)にし、色を変え、LINEへ通知する
      if (withinTime(event.getStartTime(), event.getEndTime())) {
        // タイトルの変更
        // ”(”の文字の位置を取得する(http://cya.sakura.ne.jp/js/string.htm#indexOf)
        result = event.getTitle().indexOf("(");
        debug('NotifyNotTaking: result=' + result);
        // "("以降の文字を切り取る。
        results = event.getTitle().substr(0,result);
        debug('NotifyNotTaking: results=' + results);
        // タイトルを○○の薬(済)に変更する。  
        event.setTitle(results+'(済)');
        // 色を青に変更する。
        event.setColor(CalendarApp.EventColor.BLUE);
        //LINEに送るメッセージ
        LINE_notify(timing+'のお薬を飲みました。');
      } else {
        // 現在時刻と開始時刻(start_time)~終了時刻(end_time)を比較し、時間外であればベアボーン上で稼働している発話PUSHサーバ(example.js)へ発話依頼する。
        var message = '時間外に「' + timing + 'のお薬をのみました」とのメッセージを受け取りました。この要求を無視します。';
        debug('NotifyNotTaking: ' + message);
        LINE_notify(message);
      }
    }
  });
};

/**
 * 現在時刻が開始時刻(start_time)~終了時刻(end_time)の間にあるかどうかをチェックする
 * @param  {Date}   start_time     日付
 * @param  {Date}   end_time       日付
 * @return {Boolean}               true:現在時刻が開始時刻(start_time)~終了時刻(end_time)の間にある
 *                                 false: 〃 間にない
 */
function withinTime(start_time, end_time) {
  var current_time = new Date();
  var result = (start_time <= current_time && current_time <= end_time) ? true : false;
  return result;
}

// google-home-notifyにメッセージを送るファンクション
function GOOGLE_HOME_notify(message){
  var options =
  {
    "method"  : "post",
    "payload" : "text=" + message
  };

  // ngrokで払い出された公開用URLxxxxxxxxを書き直す
  UrlFetchApp.fetch("https://xxxxxxxx.ap.ngrok.io/google-home-notifier",options);
}
リスト2.WebAPI (A2) の修正

関数NotifyNotTaking()では、現在時刻が服用開始時刻~終了時刻の間かどうかwithinTime()関数を用いてチェックして、もし外れていたらGOOGLE_HOME_Notify()関数を使って発話PUSHサーバへ発話要求を行う。

 GOOGLE_HOME_Notify()のUrlFetchApp.fetchメソッドのurlはベアボーン上で稼働する発話PUSHサーバのurlである。これはngrokによって払い出されたものを使う。したがって,発話PUSHサーバが異なれば,当然このurlも書き換えなければならない。

2019年10月23日水曜日

実験4日目

問題とその解決策


Google Home Mini が発話しない


現象


朝食後の服用を促すメッセージが発話されなかった。

原因


前日、プログラムの修正を行っていたが、それにバグがあったため、crontabでスケジューリングされていた発話依頼プログラムが起動時に異常終了していた。

対応


プログラムを修正して再起動した。
$ cd pill-reminder
$ ./pill-reminder.sh &

トラブルシューティング


今回は、Google Home Mini から発話がないにもかかわらず、LINEで応答がないと通知されたため異常に気が付いた。その原因は、大学のベアボーンサーバ上では発話依頼プログラムが正常に動いていたため。

問題発見手順は以下のとおり。
  1. pill-reminderのログ(~/pill-reminder/pill-reminder.log)を確認
  2. forever listと打ち込んでforeverのログファイル名を確認
  3. foreverのログを確認
  4. crontabのログ(/var/log/cron.log)を確認
  5. プロセスが動いているかどうかを確認($ ps aux | grep pill)
今回は上記手順5でpill-reminder.jsが動いていないことから原因が判明した。プログラムを修正して再起動した。



2019年10月22日火曜日

実験3日目

問題とその解決案


応答無になってしまう


現象


AIスピーカーが「朝の食後の薬を飲んでください」と服薬を促し、それに対して「朝のお薬飲んだよ」と答えれば、Googleカレンダーに「朝の食後の薬(済)」が設定される。ところが、これが「朝の食後の薬(応答無)」に上書きされる場合がある。

考えられる原因


Google Home が「朝のお薬飲んだよ」を捉えたタイミングでWebhookによってカレンダー更新WebAPI (A2)が実行され、「朝の食後の薬(未)」から「朝の食後の薬(済)」に変更される。
一方、発話依頼プログラムの要求に応じて指定したidの予定を取り出して返すWebAPI (A3) は、催促回数が5回のとき、タイトルを「朝の食後の薬(応答無)」に書き換え、黄色にしてしまう。
その結果、折角「朝の食後の薬(済)」になったものが「朝の食後の薬(応答無)」で上書きされてしまう。

対応策


WebAPI (A3)において、「朝の食後の薬(済)」になっている場合はたとえ催促回数が5回以上になっていても「朝の食後の薬(応答無)」で上書きしないように修正する。

結果


上記の修正の結果、問題が解決された。

2019年10月21日月曜日

実験2日目

問題とその解決案


タイミング


「昼の食後の薬を飲んでください」に対して間違って「夜のお薬飲んだよ」と答えると、夜の予定が「(済)」に変わってしまう。

解決策とさらなる課題


回答を各々の予定の開始時間~終了時間の間にしか受け付けないようにする。たとえば「昼の食後の薬を飲んでください」に対して「夜のお薬飲んだよ」と回答があった場合でも、夜の服用時間帯から外れていたら「夜の薬をのむ時間ではありません。間違っていませんか?」と回答する。

しかし、そうすると、たとえば昼の薬を飲み忘れ、昼の時間帯を過ぎて飲んだ時、昼の薬が未服用のままとして記録に残る。

予定を変更する


あらかじめ設定した服用時刻を変更したい場合があるかもしれない。たとえば食事の時刻が遅くなってしまって服用時刻をずらしたい場合などである。
この場合、「30分後にもう一度伝えて」と言って予定を遅らせることができるといい。

解決策とさらなる課題


発話依頼サーバ(pill-reminder.js)は、id指定でWebAPI (A3)を叩いて得た服薬イベントの開始時刻を確認して、もしその時間になっていない場合は発話要求はせず、その時間になったら通知するようタイマーを設定する。
開始時刻を経過している場合はこれまで通り一定時間(C_INTERVAL)経過後にタイマー通知を設定して発話要求する。

WebAPI (A3) は、POSTデータに開始時刻の延期時間(delay)が入っている場合、イベント開始時刻を延期時間だけずらして書き換える。
そうでなければ今まで通りカレンダーのタイトルと色を書き換える(済、青)。

IFTTTで「後でお薬飲む」と言ったらWebAPI (A3) で開始時刻の延期をするよう設定する(Google AssistantとWebhooksの連携)。

問題は、どのようにして対象となるイベントを特定するか。直近の(未)のイベントを対象にする?





2019年10月20日日曜日

今日から実証実験を開始します

はじめに


やっと自宅での実験環境が整いました。ベアボーンサーバは昔買った安物のマシンで代用しています。ESCというメーカーが開発しているLIVAというマシンです。仕様は以下のとおりです。
  • Processor: Intel(R) Celeron(R) CPU  N2807  @ 1.58GHz
  • Memory: 2GB (DIMM DDR3 65535 MHz)
  • Storage: eMMC 64GB
  • OS: Ubuntu 16.04 LTS
また、使用するソフトウェアは以下のとおりです。
  • node 10.16.3
  • npm 6.9.0
  • google-home-notifier 1.2.0
  • ngrok 3.2.5
  • GAS (Google Apps Script)
  • Monaca
  • IFTTT

図1.ベアボーンサーバとGoogle Home Mini

実験の設定


以下の設定で実験を行う。
  • 処方内容:
    • ディオバン錠20mg 1錠
    • メバロチン錠10 1錠
  • 用法:1日3回毎食後と就寝前
  • 投与期間:毎日

所感


  • 服用状況がカレンダーに残るので、これを集計して定期的に服用記録をメールでかかりつけの病院(医師)や薬剤師(薬局)へ送ってはどうか
  • 薬品名がカレンダーのdescriptionに書かれているので患者からの問い合わせに応じて薬の名前や効用・副作用の情報などについて回答したらどうか
  • 薬局でもらった薬が切れかかったら事前にそのことを伝えて受診を促す機能を付けたらどうか

トラブル1


催促が止まらない


さっそくトラブル発生。AIスピーカーの呼びかけに対して薬を飲んだと伝えたあとも催促が続く。原因は、発話依頼プログラム(pill-reminder.js)がWebAPI A3から受信したデータ内のtaiking_statusが0のままになっているから。
発話依頼プログラム(pill-reminder.js)の起動時は、その日のGoogleカレンダーに登録されている予定を正しく取り込んでいるが、id指定で取り込んだ場合は、古い予定(WebAPI A2による更新前)を取り込んでいる。

原因判明


WebAPI A3では、下記のようにしてid指定でカレンダーからイベントを取得しようとしている。
var event = CalendarApp.getEventById(id)
ところが、eventはnullになり、予定を取得できなかった。

CalendarAppのgetEventByIdメソッドを完全マスター」を読むと、getEventByIdを使えばid指定でイベントを取得できるはずだったのだが・・・。
開発中にもid指定でイベントを取得しようとすると古いイベントのままだったという現象が起きていた。きっとこれが原因だったのだろう。

そこで、id指定でイベントを取得するCalendarApp.getEventById関数の使用は諦めて、下記のようにその日の全イベントを取得して、その中から指定したidに一致するイベントのみ取り出す方式に変えた。
/**
 * IDを指定してイベントを取得する
 *
 * @param  {String}   id            イベントid
 * @param  {Integer}  num_reminder  催促カウント
 *
 */
function getEventsById(id,num_reminder) {
  debug("getEventsById(" + id + "," + num_reminder + ")");
  
  //「の薬」とつくイベントの取得  
  //var event = CalendarApp.getEventById(id);
  var event = null;
  
  //「の薬」とつくイベントの取得  
  var events = CalendarApp.getDefaultCalendar().getEventsForDay(
    new Date(),
    {search:'の薬'}
  );  
  
  events.forEach(function(e, i, array){
    if(e.getId() == id) {
      event = e;
    }
  });  
  
  //ジェイソン形式にする 
  var pills = [{
    "title":event.getTitle(),
    "description":event.getDescription(),
    "id":event.getId(),
    "location":event.getLocation(),
    "startTime":event.getStartTime(),
    "endTime":event.getEndTime(),
    "color":event.getColor()
  }];

  var response = {
    "drug_notifies": pills,
    "num_reminder": num_reminder,    // 催促回数
    "taiking_status": event.getTitle().indexOf("(済)") < 0 ? '0' : '1',    // 服用状況 (0:未服用, 1:服用済)
    "status": "0"        // 0=成功, 1:失敗
  }
  
  debug('response=\n' + JSON.stringify(response,null,' '));
  
  // 催促の回数が5回以上になったらタイトルに「(応答無)」に変更し、色を黄色に変える
  if(num_reminder >= 5){
    result = event.getTitle().indexOf("(");  
    //"("以降の文字を切り取る。
    results = event.getTitle().substr(0,result);
    //タイトルを○○の薬(応答無)に変更する。  
    event.setTitle(results+'(応答無)');    
    //色を黄に変更する。
    event.setColor(CalendarApp.EventColor.YELLOW); 
    // LINEにメッセージを送る
    LINE_notify('お薬を飲むように' + num_reminder + '回催促しましたが応答がありません。');
  }
  return response;
}
リスト1.指定したidのイベントを取得する関数(WebAPI A3)

この変更によって今回のトラブルは解消した。

2019年10月19日土曜日

10/15 役割に分かれて作業

谷口・勇

IFTTTが動作しなくなって、時間がたったらLINEに来るものはあった。
おにぎりテストはできたが、カレンダーを介しているものは来たり来なかったりして、不安定になるので、小山さんのGASですべてLINEに送るようにした。
IFTTTを使用するのは、薬を飲んだ時に行ったらGASに送られることだけになった。

今日は全体の流れのテストを実際にしてみた。
①QRコードを読みこむ→②カレンダーに書き込まれる→③「飲んだ」と言ったらカレンダーが書き換わる→④飲み忘れていたらLINEに通知(GAS)

内田

iphoneでの送信時エラーが出ていたものの実際には送信できていた。次回までに時間設定をできるようにしたい。

小山

タイトルが(応答無)のみになる問題が出た解決したが原因が分からない。
一通り5回の催促でタイトルの変更を黄色に変更できたので次回のテストで上手くいったか確認。

平松

前回まではプログラム起動したときすでに
過ぎた時間のお知らせもしゃべっていたがif文でマイナスの時間の薬をしゃべらないようにしたためこの問題はなくなった。

2019年10月10日木曜日

10/8 役割に分かれて作業

谷口・勇

LINEがこない問題をどうにかする

~のお薬飲んだよと言ってもカレンダーが変わらない。LINEもこない
→5~10分後にテスト用のおにぎり応答LINEに来た。(時差)
カレンダーは変更されたがLINEは来ず。
IFTTTの(済)の()をはずして更新したら朝はできた。
今までは済というカレンダーの文字に反応するようにしていたが
反応されなかったり反応したりだったので
朝(済)や昼(済)など時間帯ごとのAppletを作ったらうまくいった

卒論の構成を先輩のを参考にして皆で考えてそれに沿って作っていく。

内田

完成したと思ったが最後の最後でトラブルが起きた。
詳細はこれを参照。
来週までにはどうにかしていきたい。

小山

卒論の3.3.1(仕様)を書き中


平松

今のままではプログラムが一通り完了するとそのまま終了してしまうのでcronを使って1日のはじめにプログラムを自動で起動させることにした。
はじめエラーがあり起動することができなかったがエラーを消すと起動するようになった。
これで一日のはじめにプログラムが自動で起動するようになった。

2019年10月1日火曜日

10/1 役割に分かれて作業

勇・谷口

AIスピーカーに呼びかけても朝と昼のカレンダーが書き換わらなかった。
→原因はURLが変更されていなかったからだった。
また、お薬飲んだよのLINEが9月13日のゼミの日以来来ていないことに気づいた。
夜と寝る前の薬のぶんはLINEが来るけど朝と昼のぶんのLINEがこない。
→原因はわかっていないので次回治す。
(応答なしのはしていないのでわからない)

小山

A1が多分完成
来週は内田君から送られてきたデータが書き込めるか試してもらう。
A2がうまく動いているのか不安なのでこれも次回する。

平松

カレンダーの内容を取得して服薬を促すプログラムがほとんど完成した。
プログラムが一度終了すると終了したままなので次回はその問題を解決させていきたい。

内田

大体のレイアウトは完成した。正常に読み込めているかデバック用に新たにタブを作成。
次回は誰でも使えるようにアプリ化したい。

【TIPS】 

定時になったら起動する 

定時になったら処理を起動したい場合がある。このシステムの場合、ベアボーンから時間が来たら発話依頼をするプロセスを毎日立ち上げる必要がある。なぜならば、このプロセスは起動時、一日のスケジュールを読み込んで、薬の服用を促すが、すべての予定を終えると停止する。そこで、翌日の朝一に起動してその日のスケジュールを読み込む必要がある。それを実現するためにいくつかの方法がある。

プログラムで対応する場合

すべての予定を終えると停止するのではなく、翌日の朝一に通知が届くようにタイマーをセットする。その通知が届いたら、起動時にその日のスケジュールを読み込む処理を実行する。

ジョブスケジューリングcrontabを用いる場合

Linux系のOSにはジョブスケジューリングを行う仕組み(crontab)がある。これを使って朝一になったらこのプロセスを起動するように設定する。

課題

薬局でもらったお薬手帳のQRコードを読み込んで、処方内容をGoogleカレンダーへ設定したその当日については、そのスケジュールを読み込むタイミングがない。