Raspberry PiとSORACOMで娘のお迎えを効率化！お迎え予告システムを作ってみた

## はじめに
### 娘のお迎え時、準備の待ち時間が長すぎる！！！
小学生の娘は学童に通っていますが、夕方お迎えに行くとちょうど遊び始めたタイミングだったり、帰ろうとしたらトイレに行ったりしてなかなか帰る準備ができません。親としては少しでも早く帰って夕食を食べさせたいのに・・・

![娘のお迎え 課題](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81380/c5abf364-eee9-3225-b90b-7ca20e116255.png)

娘曰く、「**いつ迎えが来るかわからない**からしょうがないじゃん！」
・・・まぁ、そうですよね。下の子もいるので時間が結構バラバラなんです。

### お迎え予告するシステムを作ってみた
もうすぐお迎えに行くのがわかったら便利かと娘に聞いてみたら、予想以上に期待されたので、うちにある部品を組み合わせて作ってみました。
![完成写真](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81380/3d9cd51a-3c33-862c-94fa-029ddcfcab33.jpeg)
私がお迎え開始前に右側のボタンを押しておくと、Raspberry Piにつないだランプが点灯するので、娘に**もうすぐ迎えに行くと知らせて、帰る準備をするように促す**作戦です。学童はにぎやかなので、音ではなく光で知らせる方法にしました。

このシステムを使うと、娘のお迎えがこうなります。
![娘のお迎え 解決策](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81380/0feafef3-1b6c-9cc5-e52f-c11c5b0f045c.png)

## システム構成

![soracom-button.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81380/d456679e-a118-b1de-fde6-2b4ca95491fd.png)

- 予告通知の送信側には、 LTE-M Button を使用
    - お迎え開始時にシングルクリック、もうすぐ到着時にダブルクリック
    - 間違って押した時は長押しで取消可能
- 予告通知の受信側には、Raspberry Pi を使用
    - 娘が学童に持って行って単体で動作するように、SIMでデータ通信し、モバイルバッテリーで駆動

## 必要な材料
| 品名 | 数量 | 備考 |
|:-----------|------------:|:------------|
| [IoT SIM](https://soracom.jp/store/5261/) | 1 | 標準サイズ（plan-D / データ通信のみ） |
| [USBデータ通信端末 AK-020](https://soracom.jp/store/5274/) | 1 | 通信用のUSBモデム |
| Raspberry Pi | 1 | 3B+で実験 |
| SDカード | 1 | Raspberry PiのOS(Raspbian)インストール済 |
| モバイルバッテリー | 1 | Raspberry Piの電源用 |
| microUSBケーブル | 1 | モバイルバッテリーからRaspberry Piに給電用 |
| [NeoPixel スルーホールLED 8mm](https://www.switch-science.com/catalog/1802/) | 1 | 通知ランプ用のLED |
| タクトスイッチ | 1 | シャットダウンとサービスリセット用 |
| [SORACOM LTE-M Button for Enterprise](https://soracom.jp/store/5206/) | 1 | 予告メッセージ送信用<br>販売開始セール時に買ったまま飾っていたもの |

**その他必要なもの**

- SORACOMアカウント
- AWSアカウント
- ラズパイ側の配線用にコネクタピンやリード線（ブレッドボードとジャンパ線）、はんだごて、圧着ペンチ等

## 構築手順
### SORACOM LTE-M Button for Enterprise の準備
- ソラコム公式ユーザーガイドの「[ボタンのデータ利用をサーバレスで実現したい ― SORACOM Funnel + AWS IoT Core パターン](https://users.soracom.io/ja-jp/guides/design-patterns/button/aws/#%E3%83%9C%E3%82%BF%E3%83%B3%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E5%88%A9%E7%94%A8%E3%82%92%E3%82%B5%E3%83%BC%E3%83%90%E3%83%AC%E3%82%B9%E3%81%A7%E5%AE%9F%E7%8F%BE%E3%81%97%E3%81%9F%E3%81%84--soracom-funnel--aws-iot-core-%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3)」を参考にしました。今回はAWS SNSを使用しないため、**ステップ3**以降を設定します。

#### IAMでAWS IoT接続用ユーザーを作成
1. 以下の設定でIAM ユーザーを作成します。
    - アクセスの種類で、「プログラムによるアクセス」を有効
    - アクセス許可で、「**AWSIoTDataAccess**」ポリシーを設定
1. 作成したIAMユーザーのアクセスキーとシークレットをメモしておきます。

#### AWS IoT Coreの画面でエンドポイントを確認
1. AWS IoT Core の管理画面で「設定」を開きます。
1. 画面に表示されている「**デバイスデータエンドポイント**」をメモしておきます。

#### SORACOMユーザーコンソールでボタン用SIMの設定
1. SIMグループ画面で、ボタン用のSIMグループを作成します。
1. SIMグループのSORACOM Air for Cellular 設定で、「バイナリパーサー設定」をONにし、フォーマットに「`@button`」と入力します。
1. SIMグループのSORACOM Funnel 設定をONにし、以下の設定をします。<br>トピック名は任意のもの（`soracom_button/#{imsi}`等）でよいですが、メモしておきます。
    - 転送先サービス: 「AWS IoT」
    - 転送先 URL: 「https://`<AWS IoT Core でメモしたデバイスデータエンドポイント>`/`<トピック名>`」
    - 認証情報: IAMで作成したユーザー情報で認証情報を登録して設定
    - 送信データ形式: 「JSON」
1. SIM管理画面で、SORACOM LTE-M Button for Enterprise のSIM（サブスクリプションが「plan-KM1」のもの）を探し、先ほど作成したSIMグループに登録します。

#### 動作確認
- 設定が終わったら、ボタンを押して動作確認をします（LEDの意味は、[製品仕様のLED表示](https://users.soracom.io/ja-jp/guides/iot-devices/lte-m-button-enterprise/specification/#led-%E8%A1%A8%E7%A4%BA)を参考）。
    - ボタンのLEDが橙に点滅後、緑になれば成功
    - 赤点滅になる場合は設定を見直し、赤点灯の場合は電波の良い場所でもう一度試す

### Raspberry Pi の準備
- Raspberry PiでUSBモデムとSIMを使うため、ソラコム公式ユーザーガイドの「[Raspberry Pi と USB モデム](https://users.soracom.io/ja-jp/guides/devices/general/raspberry-pi-dongle/)」を参考にセットアップしておきます。
- AWS IoTとSORACOM Beamの連携は、ソラコム公式ユーザーガイドの「[AWS IoT と接続する](https://users.soracom.io/ja-jp/docs/beam/aws-iot-console/)」を参考にしました。今回はAWS IoTのThingは使わず、メッセージブローカー機能のみ使用します。
- 通知用ランプには、少ない配線で複数色が扱えるためNeoPixelを使用しました。
- Raspberry Pi起動時にメッセージ受信用プログラムを自動起動するためにsystemdを使用し、サービス起動失敗時等のためにタクトスイッチでもサービス再起動ができるようにしました。

#### AWS IoTとSORACOM Beamの連携
##### AWS IoT接続用の証明書準備
1. AWS IoT Core の管理画面から、以下の設定でポリシーを作成します。
    - アクションに「iot:Connect, iot:Subscribe, iot:Receive」を指定
    - リソースARNに「*」を指定
![capture_003_29052021_141117.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81380/f6fa3861-4326-c9b4-d5e0-a387d9fc394d.jpeg)
1. AWS IoT Core の管理画面の証明書メニューから、「1-Click 証明書作成 (推奨)」を選択して証明書を作成します。
1. 証明書作成完了画面で証明書、パブリックキー、プライベートキーをダウンロードし、「有効化」ボタンをクリックして証明書を有効にしておきます。
![capture_002_29052021_140241.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81380/184a7003-c418-6c5d-3ab7-aaee0f122c2f.jpeg)
1. [AWS IoTのルートCA](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/server-authentication.html#server-authentication-certs)をダウンロードします（Amazon Trust ServicesエンドポイントのRSA 2048 ビットキー`Amazon Root CA1`でうまくいきました）。
1. 証明書作成完了画面の「ポリシーをアタッチ」ボタンをクリックし、最初に作成したポリシーを選択してアタッチします。

##### SORACOMユーザーコンソールでRaspberry Pi用SIMの設定
1. SIMグループ画面で、Raspberry Pi用のSIMグループを作成します。
1. SIMグループのSORACOM Beam設定で、「MQTT エントリポイント」を選択してエントリポイントを追加します。
1. SORACOM Beamの転送先を以下のように設定して保存します。
    - 種別: 「Standard MQTT broker」
    - プロトコル: 「MQTTS」
    - ホスト名: AWS IoT Core でメモしたデバイスデータエンドポイント
    - ポート番号: 「8883」
    - 証明書: 有効化
    - 認証情報: AWS IoT画面で作成した証明書の証明書ファイルとプライベートキーファイル、AWS IoTのルートCAファイルの中身をそれぞれコピペし、認証情報を登録して設定
1. SIM管理画面でRaspberry Piで使用するSIMを探し、先ほど作成したSIMグループに登録します。

#### NeoPixelとタクトスイッチの配線
1. NeoPixelは、Data In（GPIO 18、回路図の黄色線）、5V、GNDを配線します（Data Outは未使用）。
    - NeoPixelのピン配置は[スイッチサイエンスの販売ページ](https://www.switch-science.com/catalog/1802/)参考
1. タクトスイッチは、GPIOのあいているピン（回路図ではGPIO 26）とGNDに配線します。
![RaspiPixel.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81380/ccc0284a-f63f-7c4d-47ea-0e1f8df01087.png)

実際に使う際にブレッドボードのままでは娘が触ったり、抜いたりしてしまうので、配線を短くして壊れにくくしました。NeoPixelの線はまとめて太いストローに通し、さらに補強して使っています。
![工作.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81380/697c63c4-be7f-82f1-45d3-c5d61e363351.jpeg)

#### メッセージ受信用プログラム
- 必要なライブラリをインストールします。
    - MQTTライブラリ: [paho-mqtt](https://pypi.org/project/paho-mqtt/)
    - NeoPixel用ライブラリ: [RPi WS281x Python](https://github.com/rpi-ws281x/rpi-ws281x-python)

```
$ sudo pip3 install paho-mqtt
$ sudo pip3 install rpi_ws281x
```

- MQTT のトピックを Subscribe し、メッセージ受信時にNeoPixelの色を変えるプログラムを用意します。

```python
import json
import argparse
import paho.mqtt.client as mqtt
from rpi_ws281x import PixelStrip, Color

# NeoPixel
led_pin = 18
color_green  = Color(255, 0, 0)
color_red    = Color(0, 255, 0)
color_yellow = Color(255, 255, 0)
luminance_dark   = 2
luminance_bright = 255

class NeoPixelLed:
    def __init__(self, led_pin):
        self._strip = PixelStrip(1, led_pin)
        self._strip.begin()

    def off(self):
        self.changeColor(Color(0, 0, 0))

    def changeColor(self, color, brightness=None):
        if brightness is not None:
            self._strip.setBrightness(brightness)
        self._strip.setPixelColor(0, color)
        self._strip.show()

# MQTT接続時の処理
def on_connect(client, userdata, flags, rc):
    client.subscribe('soracom_button/#', 1) # トピック名はボタン用SIMの転送先に指定したもの
    userdata['led'].changeColor(color_green, luminance_dark)

# メッセージ受信時の処理
def on_message(client, userdata, msg):
    payloads = json.loads(msg.payload.decode('utf-8')).get('payloads', {})
    type = payloads.get('clickTypeName')
    if type == 'SINGLE':
        userdata['led'].changeColor(color_yellow, luminance_bright)
    elif type == 'DOUBLE':
        userdata['led'].changeColor(color_red, luminance_bright)
    elif type == 'LONG':
        userdata['led'].changeColor(color_green, luminance_dark)

if __name__ == '__main__':
    led  = NeoPixelLed(led_pin)

    # --stop オプション指定時はNeoPixelを消灯
    parser = argparse.ArgumentParser()
    parser.add_argument('--stop', action='store_true', help='Turn off LED')
    args = parser.parse_args()
    if args.stop:
        led.off()
    else:
        try:
            # MQTT接続
            mqttc = mqtt.Client(userdata={'led': led})
            mqttc.on_connect = on_connect
            mqttc.on_message = on_message
            mqttc.connect('beam.soracom.io', port=1883)
            mqttc.loop_forever()
        except Exception as e:
            print(e)
            led.off()
```

- 動作しているのがわかるようにMQTT接続時にNeoPixelを暗めで緑点灯しています。

#### メッセージ受信用プログラムの自動起動設定
- systemd用のユニットファイルを用意します。
    - オンライン後に起動するため、Afterで `network-online.target` を指定しました。
    - pppで接続する場合(SIM通信)、オンライン後の起動だけではタイミングが早すぎたため、RestartSecで起動失敗時のリトライ間隔を長めに設定しました。
    - Raspberry Pi起動直後にサービス起動する際、名前解決サービス？が間に合わず、MQTT接続時に「Temporary failure in name resolution」というエラーが出るのですが、解決できていません（初回は失敗しますが、リトライ時につながります）。
    - サービス停止時にLEDが付きっぱなしになるのを防ぐため、ExecStopPostで終了時の処理（LEDを消すコマンド）を指定しました。

```:soracom-button.service
[Unit]
Description = SORACOM button event notify system
Requires = network-online.target
After = network-online.target

[Service]
Type = simple
ExecStart = /usr/bin/python3 /path/to/button_notify.py
ExecStopPost = /usr/bin/python3 /path/to/button_notify.py --stop
Restart = always
RestartSec = 5

[Install]
WantedBy = multi-user.target
```

- ユニットファイルをsystemdのサービスとして登録します。

```
$ sudo systemctl link /path/to/soracom-button.service
```

- サービスを有効化し、起動します。

```
$ sudo systemctl enable soracom-button
$ sudo systemctl start soracom-button
```

#### シャットダウンとサービス再起動のプログラム
- JellyWareさんのクラゲのIoT「[RaspberryPiにshutdownボタンを付けよう](https://jellyware.jp/kurage/raspi/shutdown_button.html)」を参考にして、短押し時にサービスを再起動、長押し時にRaspberry Piをシャットダウンするプログラムを用意しました。ピン番号（GPIO18からGPIO26）と長押し判定時間（500msから1s）、短押し時の処理以外は参考記事のソースコードのままです。

```python:shutdown.py
import time
import RPi.GPIO as GPIO
import os

shutdown_pin = 26

GPIO.setmode(GPIO.BCM)
GPIO.setup(shutdown_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)

while True:
    GPIO.wait_for_edge(shutdown_pin, GPIO.FALLING)
    sw_counter = 0

    while True:
        sw_status = GPIO.input(shutdown_pin)
        if sw_status == 0:
            sw_counter = sw_counter + 1
            # 1秒以上の長押し
            if sw_counter >= 100:
                os.system("sudo shutdown -h now")
                break
        # 短押し
        else:
            os.system("sudo systemctl restart soracom-button")
            break

        time.sleep(0.01)
```

- `/etc/rc.local` の `exit 0`より上に起動用コマンドを追記します。

```:/etc/rc.local
... 前略...
/path/to/shutdown.py

exit 0
```

## 動作確認
- Wi-Fiを無効にした状態で、SIMをいれたUSBモデムと電源をRaspberry Piにつなぎ、動作確認をします。
- メッセージ受信用プログラム（soracom-buttonサービス）が自動起動して、MQTTサーバーに接続成功するとNeoPixelが緑に点灯します。
- LTE-M Buttonを押した際の動作を確認します。
    - 1回押すと、NeoPixelが黄色く点灯
    - 2回押すと、NeoPixelが赤く点灯
    - 長押しすると、NeoPixelが緑点灯に戻る
- 手動でsoracom-buttonサービスを停止すると、メッセージ受信用プログラムが終了し、NeoPixelが消灯します。
- タクトスイッチを短押しすると、soracom-buttonサービスが再起動し、NeoPixelが緑に点灯します。
- タクトスイッチを長押しすると、シャットダウンコマンドが実行され、Raspberry Piが停止します。
- 給電開始からsoracom-buttonサービスの初回起動の間、NeoPixelが意図せず点灯してしまう問題がありますが、まだ解決していません。

## 実証実験
- 娘に使い方を教え、学童に持って行かせて実験をしました。放課後、学童に着いたら、USBモデムとモバイルバッテリーにつないでいるようです。
- 実際のお迎え時にボタンを押して、娘にお知らせをすることができました。NeoPixelは明るくて目立つので、注意していなくても意外と気づくようです。
- 娘が学童で光物付精密機器を使っているのが指導員さん達にバレましたが、**指導員さん達もいつ迎えに来るのかわかると便利！**だそうで、児童が少なくなる夕方以降は机の上に出しておくように勧められていました。（娘のお迎えはだいたい最後なので…）
- まだ1週間しか試していませんが、私が学童到着してから娘が出てくる時間は短縮されています。そのおかげで、夕食から寝かしつけまでの時間に少し余裕ができました。

## あとがき
- LTE-M Buttonでお迎えを知らせるのは、娘専用帰宅準備リモコンのようで面白い！
- 娘が学童に着いてRaspberry Piを起動させると、SORACOMユーザーコンソールでSIMがオンライン状態になるので、遠隔でも動いているのがわかって良かったです。
- SORACOM Beamは初めて使いましたが、Raspberry Pi上にAWS IoT接続用の証明書を保存したり、プログラムであれこれ指定する必要がないのが便利でした。
- 娘から小型化の要望が出たので、Raspberry Pi Zeroでも試してみるつもりです。