見出し画像

LINEのBOTで実験の声かけしてみる(3)

1時間の実験を30分の実験にカンタンに変更できるようにしようと作戦を練っています。ところが友達に頼んで実験のリハーサルをしていたら、うまく動かない、途中で止まっていることが分かりました。そこでGoogleスプレッドシートに作っておいた ”log” シートを覗いてみると……

デバッグ

IDがU4dで始まるAさんの23:52:23の「開始します」で始まった実験は赤丸の 1→2→3→4まで進んだが5の「良いペースです」が来ない。IDがUb8で始まるBさんの0:09:10の「開始」で始まった実験は緑丸の1→2で止まっていることがわかる。

夜も遅いので翌日も同様に試してみる。IDがUd4から始まるAさんは11:23:23の「開始」から順調に1〜8まで進んで無事に終了(赤丸)。IDがUd8で始まるBさんの11:50:03の「開始します」で始まった実験は4の「その調子です」までで止まってしまった(緑丸)。Bさんが再チャレンジして13:47:17に「開始します」で始まった実験は1→2で止まってしまった(青丸)。再再チャレンジも同じ(紫丸)。

制限

GASのトリガー(時計マーク)を見てみると [時間ベース]のイベントが多数列挙されていた。そのほとんどが [無効]となっている。さらに調べてみると、無効になっているのは既に時間が過ぎてしまったタイマーのトリガーだと分かった。

まめ1号の5分の制限時間のようにGASに他にも制限があるようだ。調べてみると…

ありました、ありました。スクリプト=プログラムあたり20個が上限のようです。

  • タイマートリガーは20個まで(今回の制限)

  • 1回の実行時間は6分(5分じゃなく6分が正しかった)

  • トリガーの実行時間は1日90分(これ大丈夫かな?)

1回のトリガーで実行される時間を5秒とする。5回の声がけと最後の終了のお知らせを入れて6回のタイマーのトリガーが実行される。1回の実験で5秒x6回で30秒。タイマーが20個なので最大20人が同時に実験するとして、30秒 x 20人で600秒=10分。上限は90分なので20人の被験者が1日に9回まで実験できる計算。十分!
下図はスクリプト=プログラムの実行時間。平均5秒はかかっていないようだ。

Google本家ではないですが、わかりやすい日本語の記事も見つけたので参考までに。

本当にタイマーの制限だったのか確かめてみる

本当に20個の上限だったのか数えてみました。深夜の実験は20個目で止まってました。

昼間の実験も20個目で止まってました。ビンゴです。

GASの[実行数]をチェックすると「このスクリプトに含まれているトリガーの数が多すぎます。さらに追加するには、スクリプトからトリガーを削除する必要があります」というエラーメッセージが出ていました。

つまり「タイマーのトリガーは使い終わったら処分しなさい」ということ。今まで気がつかなかったのは、日が変わると(日をまたぐと?)無効なタイマーは勝手に処分してくれていたからのようです。Googleさんが旨い具合にやってくれてたおかげ。

タイマーをお片付けする改良版プログラム

function postMessageStepZ(userId, stepNum) {  // ステップ数(stepNum)に応じたメッセをユーザ(userId)に送る
  if ( stepNum == 0 ) {
    postMessageZ( userId, "では実験を始めてください。");
  }
  else if ( stepNum == 1 ) {
    postMessageZ( userId, "順調ですか?");
  }
  else if ( stepNum == 2 ) {
    postMessageZ( userId, "その調子です。");
  }
  else if ( stepNum == 3 ) {
    postMessageZ( userId, "良いペースです。");
  }
  else if ( stepNum == 4 ) {
    postMessageZ( userId, "もう少しです。");
  }
  else if ( stepNum == 5 ) {
    postMessageZ( userId, "頑張ってください。");
  }
  else if ( stepNum == 6 ) {
    postMessageZ( userId, "お疲れ様でした。");
  }
  else {
    postMessageZ( userId, "hogehoge"); // ここには到達しないはず。ステップ数(stepNum)は0未満や7以上ならない
  }
}

function mkIntervalZ() { // 次の声がけまでの時間をランダムに決める
  var rrr;
  rrr = Math.round(7 + Math.random() * 4); // 7分 + ランダムで0,1,2,3,4分
  return rrr;
}

function setNextScheduleZ( userId, stepNum, progressTime) {  // 次のタイマーのスケジュールを設定
  const functionName = 'execQueueTopZ';
  var iii;
  if ( stepNum < 6) {  // ステップ数(stepNum)が1,2,3,4,5の時はランダム
    iii = mkIntervalZ();
  }
  else {               // ステップ数(stepNum)が6の時は60分から経過時間(progressTime)を引いた残り時間
    iii = 60 - progressTime;
  }
  var ddd = new Date();
  ddd.setMinutes(ddd.getMinutes()+iii);
  var timestamp = Utilities.formatDate( ddd,'Asia/Tokyo', 'HH:mm:ss');
  var trigger = ScriptApp.newTrigger(functionName).
    timeBased().
    after(iii * 60 * 1000).
    create();
  var triggerId = trigger.getUniqueId();
  addQueueZ( userId, timestamp,stepNum, progressTime + iii, triggerId);  // 待ち行列queueに記録しておく
  sortQueueZ(); // 待ち行列queueを並べ替え
}

function execQueueTopZ() {  // 待ち行列queueの先頭=1行目を実行
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("queue");
  var range = sheet.getRange(1,1,1,5);
  var vvv = range.getValues();
  Logger.log(vvv);
  var userId = vvv[0][0];
  var fireTime = vvv[0][1];
  var stepNum = vvv[0][2];
  var progressTime = vvv[0][3];
  var triggerId = vvv[0][4];
  delQueueTopZ(); //先頭=1行目を削除
  postMessageStepZ( userId, stepNum);
  var allTriggers = ScriptApp.getProjectTriggers();
  for (var iii = 0; iii < allTriggers.length; iii++) {
    if (allTriggers[iii].getUniqueId() == triggerId) {
        ScriptApp.deleteTrigger(allTriggers[iii]);
    }
  }
  if ( stepNum < 6) { // ステップ数(stepNum)が1,2,3,4,5の時は次のタイマーのスケジュールを作成
    setNextScheduleZ( userId, stepNum+1, progressTime);
  }
  
}

function sortQueueZ() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("queue");
  var range = sheet.getRange(1,1,20,5);
  range.sort(2); // range.sort({column: 2, ascending: true})
}

function delQueueTopZ() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("queue");
  var range = sheet.getRange(1,1,1,5);
  range.deleteCells(SpreadsheetApp.Dimension.ROWS);
}

function delQueueAllZ() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("queue");
  var range = sheet.getRange(1,1,20,5);
  range.deleteCells(SpreadsheetApp.Dimension.ROWS);
}

function addQueueZ( userId, nextTime, stepNum, progressTime, triggerId) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("queue");
  sheet.appendRow([userId, nextTime, stepNum, progressTime, triggerId]);
}


function getTokenZ() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("secret");
  var token = sheet.getRange(1, 2).getValue();
  Logger.log( token);
  return token;
}

function logZ( uuu, xxx) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("log");
  var timestamp = Utilities.formatDate( new Date(),'Asia/Tokyo', 'HH:mm:ss');
  sheet.appendRow([timestamp, uuu, xxx]);
}

function postMessageZ( ttt, mmm) { // 相手(ttt)にメッセ(mmm)を送る
  logZ( ttt, mmm)
  const url = 'https://api.line.me/v2/bot/message/push';
  const payload = {
    to: ttt, 
    messages: [{
      type: 'text',
      text: mmm 
    }]
  };
  const params = {
    method: 'post',
    contentType: 'application/json',
    headers: {
      Authorization: 'Bearer ' + getTokenZ()
    },
    payload: JSON.stringify(payload)
  };

  UrlFetchApp.fetch(url, params);
}

function doPost(e) {
  let token = getTokenZ(); 
  let json = JSON.parse(e.postData.contents);
  let userId = json.events[0].source.userId;
  let userMessage = json.events[0].message.text;
 
  logZ( userId, "受信:"+userMessage);
  if ( userMessage.match(/開始/)) {
    postMessageStepZ( userId, 0);
    setNextScheduleZ( userId, 1, 0);
  }
  else {
    postMessageZ( userId, "何?");   
  }
} 

プログラムを少し解説。
待ち行列queueを実行する関数 execQueueTopZ() に使い終わったタイマーを掃除するコードを追加。まずトリガーの一覧を取得して、その数だけfor ループを回します。一覧のトリガーのID(トリガーごとに固有の番号がついています)と今実行しているトリガーのIDが一致すればそのトリガーを削除しています。

var allTriggers = ScriptApp.getProjectTriggers();
  for (var iii = 0; iii < allTriggers.length; iii++) {
    if (allTriggers[iii].getUniqueId() == triggerId) {
        ScriptApp.deleteTrigger(allTriggers[iii]);
    }
  }
        :

「今実行しているトリガー」をスプレッドシート queue のE列(5列目)に格納するように変更してあります。関数setNextScheduleZ()ではトリガーを作るときにトリガーIDを取得し、関数addQueueZ() の5番目の引数に渡してスプレッドシート に書き込むようにしました。

        :
  var triggerId = trigger.getUniqueId();
  addQueueZ( userId, timestamp,stepNum, progressTime + iii, triggerId); // 待ち行列queueに記録しておく
        :

これでしばらく使ってみてさらに改良してみよう。次回へ続く!

この記事が気に入ったらサポートをしてみませんか?