6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ツンデレ「けんこちゃん」が僕を健康管理してくれる話(バックエンド編)

Last updated at Posted at 2021-12-23

われわれ丑之日プロジェクトが2021年9月26日に予選が開催された「Digital Hack Day 2021」に参加して24時間で開発し、73組の中から決勝まで勝ち残った「けんこちゃん」のアプリ側のバックエンドについて書かせていただきます。
ガジェット側の組み込み系については「ツンデレ「けんこちゃん」が僕を健康管理してくれる話(組み込み編)」をご覧ください。

DigitalHackDayについて

「Digital Hack Day 2021」については有名なHackathonですので、詳細の説明は省かせていただきます。今回のDigital Hack Dayのテーマは日本のデジタル化でした。最優秀賞に選出されるための審査基準は、「課題解決」「Hack」「Fun」の3つでした。(詳細はHackDayのホームページをご覧ください)
われわれ丑之日プロジェクトは参加した3人で脳みそを絞り、日本人の健康問題にメスを入れることを決心しました。題して「けんこちゃん」。審査基準へのアンサーは以下の通りです。

課題解決: 塩分の取りすぎ
Hack: 調味料置き場
Fun: ツンデレ

意味がわからないと思います。百聞は一見に如かず、実際の決勝での発表をご覧ください。

けんこちゃんについて

ソフトウェアもハードウェアも3人で限られた時間で開発しました。

システム

まずは簡単にシステムの概要をご覧ください。

使用したモノ

マイコン: Wio LTE JP Version - 4G, Cat.1
ロードセル: ARCELI HX711 1KG
LCD: Grove - 16x2 LCD (White on Blue)
3Dプリンタ: Creality Ender 3 Pro
フィラメント: Pxmalion Wood 1.75mm

構成図

システム構成.png
Wio LTEで計測した調味料の重さをGASにHTTP POST、その結果をSpread Sheetsに保存した後にその日の調味料の摂取量をLINE Messaging APIを使用してユーザーに通知、さらにはAngularからGASにGETリクエストすることで様々なデータをレスポンシブに表示することができる、というものです。

シーケンス図

以下のようなシーケンスを考案しました。
sequence.png

我々がプログラムを書いたのは以下の3箇所です。
・ガジェット: Arduino
・アプリ バックエンド: Google Apps Script
・アプリ フロントエンド: TypeScript

本項ではアプリ側のバックエンド、前項ではガジェット側の組み込み系のプログラムについて駄文を弄させていただきます。

バックエンドの機能

今回はDigital Hack Dayの協賛企業の中から、LINEさんのLINE Messaging APIを使用させていただき、Google Apps Scriptを用いてバックエンドを実装しました。バックエンドで実現したいのは

  1. ガジェットからの調味料の重さのPOSTリクエストを受ける
  2. SpreadSheetsに重さ等の情報を保存
  3. LINE Messaging APIにメッセージをPOST

この3つです。

大事なことをお伝えします。Hackathonの限られた時間で未熟者が作ったモノなので、変数名が適当だったり、余計な文が入っていたりします。宗教上の理由でこれらを受け付けない方はそっとブラウザバックしてください…。その他の方はコメントでご指摘ください。まじで助かります。

開発の準備

Google Spread Sheets

まず、ユーザーの調味料の重さなどのデータを保存するのに使うSpreadSheetsを作成するところから始めます。
普段と同じようにSpreadSheetsを作成したら、メニューバーにある「拡張機能」から「Apps Script」をクリックして、Google Apps Scriptのエディタを開きます。Apps Script.png

LINE Messaging API

LINE Developersにアクセスし、Messaging APIのチャンネルを作成します。個人的な目的での使用程度の利用であれば、料金は発生しないと思います。これはたくさん遊ぶしかない…!!
詳細はドキュメントをご覧ください。

ガジェットからのPOSTリクエストの処理

DigitalHackDay2021-API.ts

function doPost(e) {
  var json = JSON.parse(e.postData.getDataAsString());

  /** 受信したメッセージ情報を変数に格納する */
  var validate;
  try {
    var timestamp = getUnixTime();
    var measured_salt = json["salt"];
    var measured_suger = json["suger"];
    validate = true;
  } catch (e) {
    validate = false;
  }
}

function getUnixTime(){
  const now = new Date();
  Logger.log(`now: ${now}`);

  const formatNow = Utilities.formatDate(now, 'GMT', 'dd MMM yyyy HH:mm:ss z');
  Logger.log(`formatNow: ${formatNow}`);

  const unixTime = Date.parse(formatNow)/1000;
  Logger.log(`unixTime: ${unixTime}`);
  Logger.log(`unixTime: ${unixTime.toFixed()}`);
  return unixTime.toFixed();
}

GASにデータがPOSTリクエストされると、function doPost(e) {}が呼ばれます。
今回はJSON形式で調味料のデータをPOSTしているのでJSON.parse(e.postData.getDataAsString()でデータをパースします。
後の処理のためにvalidateを定義し、Unix時刻と、パースしたデータから砂糖の重さを取得します。
これでPOSTされたデータをGASで扱えるようになりました。

SpreadSheetsに取得した情報を保存

無事にJSONから重さ等のデータを取得できた場合、SpreadSheetにデータを保存します。

DigitalHackDay2021-API.ts
function saveDataToSpreadSheet(timestamp, measured_salt, measured_suger){
  const sheet = SpreadsheetApp.getActiveSheet();
  let lastRow = sheet.getLastRow();

  data = [[timestamp, measured_salt, measured_suger]];
  sheet.getRange(lastRow+1, 1, 1, 3).setValues(data);
}

SpreadsheetApp.getActiveSheet()で現在アクティブなSpreadSheet、つまりデータの保存用に作成したSpreadSheetを指定します。
今回はPOSTされてきたデータをどんどん新たに追加していきたいので、getLastRow()でSpreadSheetの中で何かしら記載のある最後のRow番号を取得します。
保存したいデータを2次元リストとして作成し、getRange(row, column, numRows, numColumns)1でデータを書き込む範囲を指定して、setValues()でデータを書き込みます。

LINE Messaging APIにメッセージをPOST

LINE Developersにアクセスし、作成したチャンネルのMessaging API設定のタブをクリック。
Messaging API設定.png
一番下までスクロールすると**チャネルアクセストークン(長期)**という項目があるので、発行します。

DigitalHackDay2021-API.ts
var CHANNEL_ACCESS_TOKEN = 'CHANNEL_ACCESS_TOKEN';
var line_endpoint = 'https://api.line.me/v2/bot/message/reply';
var line_user_id = 'line_user_id';

コードの文頭でLINE Messaging APIの利用に必要な情報を定義します。
CHANNEL_ACCESS_TOKENに、先ほど発行した**チャネルアクセストークン(長期)**を代入します。
line_user_idに、LINEメッセージを送信したいLINEアカウントのユーザーID2を代入します。このユーザーIDは、普段ともだち追加などの際に使用するIDとは異なるので気をつける必要があります。

DigitalHackDay2021-API.ts

/** LINEメッセージを配信 */
function sendPushLine(user_id, message_content) {
  if (!user_id) {
    var user_id = "line_user_id";
  }
  if (!message_content) {
    var message_content = '空のメッセージを送信します。'
  }

  var url = "https://api.line.me/v2/bot/message/push";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
  };

  var postData = {
    "to" : user_id,
    "messages" : [
      {
        'type':'text',
        'text': message_content,
      }
    ]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);
}

リファレンスを参考に、user_id(ユーザーID)と message_content(メッセージ内容)を指定してプッシュ通知ができるfunctionを用意します。

あとはメッセージ内容を作成すればLINE Messaging API向けのバックエンドは完成です!

DigitalHackDay2021-API.ts

var CHANNEL_ACCESS_TOKEN = 'CHANNEL_ACCESS_TOKEN';
var line_endpoint = 'https://api.line.me/v2/bot/message/reply';
var line_user_id = 'line_user_id';

function doPost(e) {
  var json = JSON.parse(e.postData.getDataAsString());

  /** 受信したメッセージ情報を変数に格納する */
  var validate;
  try {
    var timestamp = getUnixTime();
    var measured_salt = json["salt"];
    var measured_suger = json["suger"];
    validate = true;
  } catch (e) {
    validate = false;
  }

  var result;
  if (validate) {
    try {
      saveDataToSpreadSheet(timestamp, measured_salt, measured_suger); // DBに計測値を保存
      message_content_reporting = getReportingMessageContent(measured_salt, measured_suger);
      took_amounts = getTookAmount(measured_salt, measured_suger);
      took_salt = took_amounts[0];
      took_suger = took_amounts[1];
      message_content_calculating = getCalculatedMessageContent(took_salt, took_suger);
      // message_contents = message_content_reporting + "\n\n" + message_content_calculating;
      /** LINEのuser_idを取得してプッシュ通知 */
      var user_id = line_user_id;
      sendPushLine(user_id, message_content_reporting);
      sendPushLine(user_id, message_content_calculating);

    } catch(e) {
      // 例外エラー処理 
      Logger.log('Error:')
      Logger.log(e)
      var result = e;
      return result;
    }

  } else {
    message_content = '申し訳ございません、エラーです。';

    var user_id = line_user_id;
    sendPushLine(user_id, message_content);
  }
}

/** LINEメッセージを配信 */
function sendPushLine(user_id, message_content) {
  if (!user_id) {
    var user_id = "line_user_id";
  }
  if (!message_content) {
    var message_content = '空のメッセージを送信します。'
  }

  var url = "https://api.line.me/v2/bot/message/push";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
  };

  var postData = {
    "to" : user_id,
    "messages" : [
      {
        'type':'text',
        'text': message_content,
      }
    ]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);
}

function getReportingMessageContent(measured_salt, measured_suger){
  message_content = "お塩が残り" + measured_salt + "g、お砂糖が残り" + measured_suger + "あるわね。";
  return message_content;
}

function getCalculatedMessageContent(measured_salt, measured_suger){
  message_content = "…って、あんたバカ!?\n今日だけでお塩" + measured_salt + "g、お砂糖" + measured_suger + "gも使ってんじゃないの!!";
  return message_content;
}

function saveDataToSpreadSheet(timestamp, measured_salt, measured_suger){
  const sheet = SpreadsheetApp.getActiveSheet();
  let lastRow = sheet.getLastRow();
  
  data = [[timestamp, measured_salt, measured_suger]];
  sheet.getRange(lastRow+1, 1, 1, 3).setValues(data);
}

function getTookAmount(measured_salt, measured_suger){
  const sheet = SpreadsheetApp.getActiveSheet();
  data = sheet.getRange(2, 2, 1, 2).getValues();
  took_salt = data[0][0] - measured_salt;
  took_suger = data[0][1] - measured_suger;
  return [took_salt, took_suger]
}

function getUnixTime(){
  const now = new Date();
  Logger.log(`now: ${now}`);

  const formatNow = Utilities.formatDate(now, 'GMT', 'dd MMM yyyy HH:mm:ss z');
  Logger.log(`formatNow: ${formatNow}`);

  const unixTime = Date.parse(formatNow)/1000;
  Logger.log(`unixTime: ${unixTime}`);
  Logger.log(`unixTime: ${unixTime.toFixed()}`);
  return unixTime.toFixed();
}

アプリをデプロイ

デプロイ.png
GASのエディタの右上にあるデプロイボタンを押して作成したアプリをデプロイします。アプリをデプロイするとURLが発行されるので、このURLをガジェット側で組み込まれているコードWEBHOOK_URLに代入します。

github.com/UshinohiProject/DigitalHackDay2021-WioLTE
#define WEBHOOK_URL       "https://script.google.com/macros/s/****..."

これでガジェット側からGASに調味料のデータをPOSTリクエストし、GAS側でそのデータを受け取ることができるようになりました。

まとめ

GASでアプリを作るたびに思うのですが、こんなに簡単でこんなに便利なモノが無料で使えるなんて、もうGoogle様に足を向けて寝られません…。Hackathonではスピード感を持ってトライ&エラーを繰り返してバグを消していくことが大事になると思っているので、GASはHackathonでWEBアプリを作成する際にピッタリの選択肢だといつも痛感します。
また、LINE Messaging APIもシンプルな使用感で普段から身近にあるLINEというSNSを気軽に自作したコードから扱えるというのも、とてつもなく魅力的だと思います!

関連リンク:
ツンデレ「けんこちゃん」が僕を健康管理してくれる話(組み込み編)
ツンデレ「けんこちゃん」が僕を健康管理してくれる話(バックエンド編)

#丑之日プロジェクト
私たち丑之日プロジェクトはAnii、Mark、Taroの3人で3Dプリンター鉄の棒をノコギリで切るところから自作したりする様子をYouTubeに投稿しているモノづくり集団です!ぜひチャンネル登録して、役には立たないがなんだか楽しいものをたくさん発明する僕らを応援してください!よろしくお願いします!!

  1. GetRange()の使い方の詳細はリファレンスを参照

  2. 技術ブログなどを参考に、自力で確認する必要があります。

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?