IoT歯ブラシスタンドを作ってみた

健康寿命を延ばし、生き生きとした生活を送るためには、歯磨き週間は大変重要です。過去の調査でも、自立した高齢者の多くは、食後に歯を磨き、早寝早起き、1日3食を規則正しく摂り、かかりつけ歯科医をもっていて、喫煙をしないという習慣が身についています。
ここでは、歯磨き意識を高め、また家族や周囲からも歯磨きを通して、高齢者を見守る仕組みとして「IoT歯ブラシスタンド」を作ってみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/0ce31337-c36d-ee1d-d0f2-230f3c20a79e.png)

#全体イメージ
![Toothbrush.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/eb20f490-d257-142d-e93b-e8ba343dff93.png)
IoT歯ブラシスタンドで検知した歯ブラシの動きをSigfoxネットワークで送信し、（今回は）Google Cloud PlatformにCallback。GCPのCloud FunctionsでLINE Messaging APIにPOSTするとともに、BigQueryに蓄積するという流れです。

#必要機器
|機器|イメージ|価格|
|:---|:---|:---|
|[M5Stack](https://www.switch-science.com/catalog/3647/)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/f49c2d7b-ea61-8bc3-81ae-4ee1caa7f54d.png)|4,565円|
|[M5Stack用Sigfoxモジュール](https://www.switch-science.com/catalog/6624/)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/cdab8813-623b-3e2b-1f7c-65009eb696fe.png)|5,500円|
|[M5Stack用PIRセンサユニット](https://www.switch-science.com/catalog/5697/)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/42ac45df-9b36-1e2e-f9e6-572551c2b606.png)|583円|
|[珪藻土ハブラシスタンド](https://www.nitori-net.jp/ec/product/8511391s/)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/3cb28051-512b-a55f-d91f-b7bc00c2e2d7.png)|1,212円|
あとは、工具やネジなど。珪藻土はキリでも穴を開けれるので便利です。

#M5Stack側スケッチ
M5Stackでは、PIRセンサ（ここではPIN36）からのデジタル入力をもって、歯ブラシが移動したことを判定し、Sigfox（M5Stack Com.X）にメッセージ送信のATコマンドを送っています。あわせて、歯磨き時間のタイマ表示も行っています。

```m5stack.ino.c
#include <M5Stack.h>

int last_sense = 0;
bool isBrushing = false;
unsigned long brushing_start_time = 0;

void setup() {
  M5.begin();
  M5.Power.begin();
  Serial2.begin(9600, SERIAL_8N1, 16, 17);
  Serial.begin(115200);

  M5.Lcd.clear(BLACK);
  Serial.println("IoT Toothbrush Stand: ");
  pinMode(36, INPUT);
}

void loop() {
  if (Serial2.available()) {
    String ack = Serial2.readString();
    Serial.println(ack);
    if (ack.startsWith("OK")) {
      SendMessageEnd();
    }
  }
  int sense = digitalRead(36);
  
  if(sense==1 && last_sense == 0) {
    if (isBrushing == false) {
      isBrushing = true;
      brushing_start_time = millis();
      SendMessageStart();
    } else {
      isBrushing = false;
      DrawBrushingEnd();
    }
  }
  last_sense = sense;
  DrawSensing();
  DrawBrushing();
  delay(250);

  M5.update();
}

void DrawBrushing()
{
  if (isBrushing) {
    int t = (int)((millis() - brushing_start_time) / 1000);
    M5.Lcd.drawRect(30, 120, 250, 70, WHITE);
    if (t > 5 * 60) {
      isBrushing = false;
      DrawBrushingEnd();
    } else {
      int m = (int)(t / 60);
      int s = t % 60;
      char d[32];
      sprintf(d, "%d:%02d", m, s);
      M5.Lcd.setCursor(80, 50);
      M5.Lcd.setTextSize(7);
      M5.Lcd.print(d);
      int w = (250*t)/180; if (w>=250) w = 250;
      M5.Lcd.fillRect(30, 120, w, 70, WHITE);
    }
  }  

  //画面上部に"B" or "-" (デバッグ用)
  M5.Lcd.setCursor(10, 0);
  M5.Lcd.fillRect(10, 0, 10, 10, BLACK);
  M5.Lcd.setTextSize(1);
  if (isBrushing) {
    M5.Lcd.print("B");
  } else {
    M5.Lcd.print("-");
  }
}

void DrawBrushingEnd()
{
  M5.Lcd.fillRect(30, 50, 250, 190, BLACK);
}

void DrawSensing()
{
  M5.Lcd.setCursor(0, 0); 
  M5.Lcd.fillRect(0, 0, 10, 10, BLACK);
  M5.Lcd.setTextSize(1);
  if (last_sense) {
    M5.Lcd.print("O");
  } else {
    M5.Lcd.print("-");
  }
}

void SendMessageStart()
{
  Serial.println("AT$SF=01");
  Serial2.println("AT$SF=01");
  M5.Lcd.setCursor(20, 0);
  M5.Lcd.fillRect(20, 0, 10, 10, BLACK);
  M5.Lcd.setTextSize(1);
  M5.Lcd.print("W");
}

void SendMessageEnd()
{
  M5.Lcd.setCursor(20, 0);
  M5.Lcd.fillRect(20, 0, 10, 10, BLACK);
}
```

#SigfoxクラウドでCALLBACK設定
次に、SigfoxクラウドでGCPへメッセージを転送するためCALLBACK設定を行います。
Sigfox Callbackに関しては、[こちら](https://qiita.com/ghibi/items/53259381a30363538a6d)を参考にしてください。
今回は、Custom Callbackを作成しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/be070529-28cc-054e-b494-f11a16ede691.png)
送信するJSON Bodyは、下記の通りとしておきます。`{data}`のところに、歯ブラシが抜かれたときに`01`が入るようになります。

```callback.json
{
  "device": "{device}",
  "time": {time},
  "data": "{data}",
  "seqNumber": {seqNumber},
  "deviceTypeId": "{deviceTypeId}"
}
```


#Google Cloud PlatformでSigfoxメッセージを受信
GCPのCloud FunctionsでHTTPトリガの関数を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/901afc31-15de-e1fe-3b76-66fd77e20f57.png)
今回は、関数のランタイムをPython3.7とし、以下mainのエントリポイントで関数を作成しました。
ここでは、Sigfoxクラウドからのコールバックを受信し、BigQueryに歯磨き時刻を記録するとともに、LINE Messaging APIを用いメッセージを送信しています。

```main.py
import os
import sys
import urllib.request
import urllib.parse
import json
import base64
from flask import abort
from rfc3339 import rfc3339
from datetime import datetime, timedelta, timezone
from google.cloud import bigquery

def main(request):
    request_dict = request.get_json()
    dt = datetime.fromtimestamp(request_dict['time'], timezone(timedelta(hours=+9), 'JST'))
    storeDB(dt, request_dict)
    html = sendLineMsg(dt, request_dict)
    print(html.decode('utf-8'))

def storeDB(dt, request_dict):
    PROJECT    = os.environ.get('PROJECT')
    DATASET    = os.environ.get('DATASET')
    TABLE      = os.environ.get('TABLE')

    output_rows = []
    output_rows.append([
        dt,
        request_dict['device'],
        request_dict['seqNumber'],
        request_dict['data']
     ])

    bq_client   = bigquery.Client(PROJECT)
    dataset_ref = bq_client.dataset(DATASET)
    table_ref   = dataset_ref.table(TABLE)
    table_obj   = bq_client.get_table(table_ref)
    errors       = bq_client.insert_rows(table_obj, output_rows)
    if errors == []:
      print("New rows have been added.")
    else:
      print("Encountered errors while inserting rows: {}".format(errors))

def sendLineMsg(dt, request_dict):
    stime = dt.strftime('%m月%d日 %H:%M')
    payload = request_dict['data']
    if payload == '01':
        text = 'はみがきはじまったよ\n' + stime 

    line_msg = {
          "messages":[
                {
                    "type":"text",
                    "text": text
                }
            ]
    }

    h = {'Content-Type': 'application/json', 'Authorization': 'Bearer <YOUR-CHANNEL-ACCESS-TOKEN>'}
    request = urllib.request.Request('https://api.line.me/v2/bot/message/broadcast', data=json.dumps(line_msg).encode("utf-8"), headers=h, method='POST')
    response = urllib.request.urlopen(request)
    print(response.getcode())
    html = response.read()
    return html
```

LINEチャネルの開発設定方法は省略しますが、以上が成功すると下図のように"はみがきはじまったよ"メッセージがLINE BOTで配信されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/04790124-8b06-aa04-c7af-b9558454d665.png)
今回は、LINEメッセージをブロードキャストしていますので、はみがきチャネルに参加した人すべてに配信されてしまいます。本来は、伝えたい方（家族など）にのみ配信できるようにすべきですね。

#振り返ってみて
今回、歯ブラシスタンドで歯ブラシが抜かれたことをPIRセンサを用いました。最初は、感圧センサ(FSR402)を使ってみたのですが、感度特性のためか、歯ブラシ程度の重さ（圧力）をうまく検知できませんでした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/118c5e11-d2fe-e382-23ec-3a78bf1314e9.png)
逆に、PIRセンサを用いることにより、検知はできるようになったのですが、当然のごとく、「抜いた」のか「刺した」のかがわからないという結果。
本当に使えるようにする場合は、軽量なものにも反応するようなハード的なスイッチを用意し、マイコンや通信モジュールのスリープ制御もできれば良いのだと思っています。
[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/d6ad10bc-75ba-8124-14d4-f144657c6473.png)](https://twitter.com/ghibi)