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

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

# DigitalHackDayについて
「Digital Hack Day 2021」については有名なHackathonですので、詳細の説明は省かせていただきます。今回のDigital Hack Dayのテーマは**日本のデジタル化**でした。最優秀賞に選出されるための審査基準は、「課題解決」「Hack」「Fun」の３つでした。(詳細は[HackDayのホームページ](https://hackday.yahoo.co.jp/#:~:text=%E8%A9%B3%E7%B4%B0%E3%82%92%E8%A6%8B%E3%82%8B%EF%BC%9E-,%E6%9C%80%E5%84%AA%E7%A7%80%E8%B3%9E,-%E8%B3%9E%E9%87%91%20300%E4%B8%87)をご覧ください)
われわれ丑之日プロジェクトは参加した３人で脳みそを絞り、**日本人の健康問題**にメスを入れることを決心しました。題して「けんこちゃん」。審査基準へのアンサーは以下の通りです。

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

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

<iframe width="560" height="315" src="https://www.youtube.com/embed/w_Fb7wVX0bA?start=749" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# けんこちゃんについて
ソフトウェアもハードウェアも３人で限られた時間で開発しました。
## システム
まずは簡単にシステムの概要をご覧ください。
### 使用したモノ
マイコン: [Wio LTE JP Version - 4G, Cat.1](https://www.seeedstudio.com/Wio-LTE-JP-Version-4G-Cat-1-p-3018.html)
ロードセル: [ARCELI HX711 1KG](https://www.amazon.co.jp/gp/product/B07MY35F92/ref=ppx_yo_dt_b_asin_title_o01_s00?ie=UTF8&psc=1)
LCD: [Grove - 16x2 LCD (White on Blue)](https://www.seeedstudio.com/Grove-16x2-LCD-White-on-Blue.html)
3Dプリンタ: [Creality Ender 3 Pro](https://www.amazon.co.jp/gp/product/B07GZ9DNDX/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1)
フィラメント: [Pxmalion Wood 1.75mm](https://www.amazon.co.jp/gp/product/B07DMJGNF4/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1)
### 構成図
![システム構成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268123/55449864-61d1-6e14-64d5-921980ee28b3.png)
Wio LTEで計測した調味料の重さをGASにHTTP POST、その結果をSpread Sheetsに保存した後にその日の調味料の摂取量をLINE Messaging APIを使用してユーザーに通知、さらにはAngularからGASにGETリクエストすることで様々なデータをレスポンシブに表示することができる、というものです。
### シーケンス図
以下のようなシーケンスを考案しました。
![sequence.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268123/df5bc5af-5b89-43cf-3546-af74ec1e45cf.png)

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

本項では**アプリ側のバックエンド**、前項では[**ガジェット側の組み込み系**](https://qiita.com/YSSkywalker/items/3489e0185cb649b8cb88)のプログラムについて駄文を弄させていただきます。
## バックエンドの機能
今回はDigital Hack Dayの協賛企業の中から、LINEさんの**LINE Messaging API**を使用させていただき、Google Apps Scriptを用いてバックエンドを実装しました。バックエンドで実現したいのは

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

この３つです。

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

### 開発の準備
#### Google Spread Sheets
まず、ユーザーの調味料の重さなどのデータを保存するのに使うSpreadSheetsを作成するところから始めます。
普段と同じようにSpreadSheetsを作成したら、メニューバーにある「拡張機能」から「**Apps Script**」をクリックして、Google Apps Scriptのエディタを開きます。![Apps Script.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268123/63a1cfff-c398-d59f-813a-335871be9e69.png)
#### LINE Messaging API
[LINE Developers](https://developers.line.biz/)にアクセスし、[Messaging API](https://developers.line.biz/ja/services/messaging-api/)のチャンネルを作成します。個人的な目的での使用程度の利用であれば、料金は発生しないと思います。これはたくさん遊ぶしかない…！！
詳細は[ドキュメント](https://developers.line.biz/ja/docs/messaging-api/overview/#line-official-account-plan)をご覧ください。

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

```typescript: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にデータを保存します。

```typescript: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番号を取得します。
保存したいデータを２次元リストとして作成し、`getRange(row, column, numRows, numColumns)`[^1]でデータを書き込む範囲を指定して、`setValues()`でデータを書き込みます。

### LINE Messaging APIにメッセージをPOST
LINE Developersにアクセスし、作成したチャンネルの**Messaging API設定**のタブをクリック。
![Messaging API設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268123/567cf0ed-16cd-1114-a4d7-3f964398e85b.png)
一番下までスクロールすると**チャネルアクセストークン（長期）**という項目があるので、発行します。

```typescript: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アカウントのユーザーID[^2]を代入します。このユーザーIDは、普段ともだち追加などの際に使用するIDとは異なるので気をつける必要があります。

```typescript: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);
}
```
[リファレンス](https://developers.line.biz/ja/reference/messaging-api/#text-message)を参考に、`user_id`(ユーザーID)と `message_content`(メッセージ内容)を指定して**プッシュ通知**ができる`function`を用意します。

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

```typescript: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268123/21b7142b-df7c-4e03-b7cd-1bd43b6288bb.png)
GASのエディタの右上にあるデプロイボタンを押して作成したアプリを**デプロイ**します。アプリをデプロイすると**URL**が発行されるので、このURLを[ガジェット側で組み込まれているコード](https://qiita.com/YSSkywalker/items/3489e0185cb649b8cb88)の`WEBHOOK_URL`に代入します。

```c#: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を気軽に自作したコードから扱えるというのも、とてつもなく魅力的だと思います！

関連リンク:
[ツンデレ「けんこちゃん」が僕を健康管理してくれる話(組み込み編)](https://qiita.com/YSSkywalker/items/3489e0185cb649b8cb88)
[ツンデレ「けんこちゃん」が僕を健康管理してくれる話(バックエンド編)](https://qiita.com/YSSkywalker/items/b21e14fbb1a96d4163e7)

#丑之日プロジェクト
私たち[丑之日プロジェクト]()はAnii、Mark、Taroの３人で**3Dプリンター**を**鉄の棒をノコギリで切るところから自作**したりする様子をYouTubeに投稿しているモノづくり集団です！ぜひ[**チャンネル登録**](http://www.youtube.com/channel/UCvTSx-2FzWu-x_wDwKjXN5A?sub_confirmation=1)して、役には立たないがなんだか楽しいものをたくさん発明する僕らを応援してください！よろしくお願いします！！
<iframe width="560" height="315" src="https://www.youtube.com/embed/KzjsY0QX_j8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

[^1]: GetRange()の使い方の詳細は[リファレンス](https://developers.google.com/apps-script/reference/spreadsheet/sheet#getrangerow,-column,-numrows,-numcolumns)を参照
[^2]: [技術ブログ](https://spicato.com/blog/10/%E3%80%90line-api%E3%80%91line-api%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9F%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E6%83%85%E5%A0%B1%E3%81%AE%E5%8F%96%E5%BE%97/)などを参考に、自力で確認する必要があります。