ラズパイとMAMORIOとLINEで出退勤を記録する仕組みを作る

#はじめに
ラズパイでMAMORIOの近接監視をしてLINEに状況を通知するのをやってみた。
近づいたり離れたりしたらLINEに通知が来るので出退勤の記録に使えるかもしれない。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252291/00382bb7-34ea-adc1-6882-08a727497d60.jpeg" width=70%>

#環境
2019/8/11時点

- ラズパイ:[Raspberry Pi 3 Model b+](https://www.amazon.co.jp/RS%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88Raspberry-Pi-3-B-%E3%83%9E%E3%82%B6%E3%83%BC%E3%83%9C%E3%83%BC%E3%83%89/dp/B07BFH96M3)
- ラズパイのOS:[raspbian stretch (2019-04-08)](http://ftp.jaist.ahttps://qiita.com/gebo/items/fdeaf11b6aefa67856e3#%E3%83%A9%E3%82%BA%E3%83%91%E3%82%A4%E3%81%A7vscode%E3%81%A7py%E3%82%92%E3%83%87%E3%83%90%E3%83%83%E3%82%AF%E5%AE%9F%E8%A1%8C%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8Bc.jp/pub/raspberrypi/raspbian/images/raspbian-2019-04-09/)	
- Python 2.7.13
- [bluez 5.50](http://www.bluez.org/release-of-bluez-5-50/)
- [code-oss]()
- [MAMORIO](https://mamorio.jp/)×2個
- iPhone6s

###補足
今回の試みは[前の記事](https://qiita.com/gebo/items/fdeaf11b6aefa67856e3)と同じ状態から始めたいと思います。以下の状態です。
- [ラズパイのバージョンについて](https://qiita.com/gebo/items/fdeaf11b6aefa67856e3#%E3%83%A9%E3%82%BA%E3%83%91%E3%82%A4%E3%81%AE%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)
- [ラズパイはここまでセットアップした状態からはじめます](https://qiita.com/gebo/items/fdeaf11b6aefa67856e3#%E3%81%93%E3%81%93%E3%81%BE%E3%81%A7%E3%82%BB%E3%83%83%E3%83%88%E3%82%A2%E3%83%83%E3%83%97%E3%81%97%E3%81%9F%E7%8A%B6%E6%85%8B%E3%81%8B%E3%82%89%E5%A7%8B%E3%82%81%E3%81%BE%E3%81%99)
- [oss-code(VSCODE)入れとくと便利(無くてもOK)](https://qiita.com/gebo/items/fdeaf11b6aefa67856e3#%E3%83%A9%E3%82%BA%E3%83%91%E3%82%A4%E3%81%A7vscode%E3%81%A7py%E3%82%92%E3%83%87%E3%83%90%E3%83%83%E3%82%AF%E5%AE%9F%E8%A1%8C%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B)

[サンプルコードはGitHubに置いときます](https://github.com/gebogebogebo/notify_ibeacon/blob/master/source/notify_ibeacon.py)

#概要図
![component.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252291/e811db69-09b0-011b-161d-598042163e2d.png)

- げぼ(人)はMAMORIO geboを持っている
- MAMORIO checkはラズパイに貼りつけておく
- げぼが近づいてきたら管理者のLINEに通知する
- げぼがどっかに行ったら管理者のLINEに通知する

#準備:LINE Notify
まずはLINEで通知する部分だけやります。
LINEにはLINE Notifyという無料のサービスがあってHTTPのリクエストからLINEメッセージを送信することができます。

###1)トークンの発行
パソコンからやります。（ラズパイのブラウザでもいいけど）

####1-1)[LINE Notify](https://notify-bot.line.me/ja/)にアクセス
https://notify-bot.line.me/ja/
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252291/124aebc6-9956-ae90-0328-a000c8cf0a83.png" width=60%>

####1-2)右上から[ログイン]する

####1-3)マイページ→[トークンを発行する]
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252291/67df37de-d6e9-ba20-d8a6-e59d93e5b2aa.png" width=60%>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252291/c0d0ea91-abb2-4bc3-c651-d5008c7d1aec.png" width=50%>

####1-4)トークンが生成されるのでメモっとく
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252291/cbaa122d-6786-8291-9004-6328beae90ff.png" width=50%>

###2)通知テスト
ラズパイから以下のpyを実行します。
access_token変数にはさっきメモったトークンを代入するようしてください。
LINEに通知がきたらテストOK

```python:notify_test.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
import requests
if __name__ == '__main__':
        access_token = "xxxxxxxxxxxxxxxxxxxxxxxxxxx"
        url = "https://notify-api.line.me/api/notify"
        headers = {'Authorization': 'Bearer ' + access_token}
        message = "テスト"
        payload = {'message': message}
        requests.post(url, headers=headers, params=payload,)
```

```bash:実行する
$ python notify_test.py
```

#準備:BlueZのセットアップ

- MAMORIOはiBeaconです。
- iBeaconはBLEのアドバタイジングという機能で自分がいることを周囲に発信することができます。
- 周囲に発信している情報は「アドバタイズパケット」という短いデータです。
- MAMORIOは30秒に1回程度、アドバタイズパケットを発信しています。
- ラズパイにはBLEが付いているのでアドバタイズパケットを受信することができます。
- ラズパイでPythonでBLEするためにBlueZというモジュールを使います。
- と、いうわけで、ラズパイでMAMORIOとお話するためにBlueZをセットアップします。
- どこかにワークフォルダを作ってその中でやっといたほうがいいかもしれません。

まずはBlueZ本体ではなく、必要なパッケージをインストールします。これは apt intall でOK。

```bash:必要パッケージのインストール
$ sudo apt install bluetooth libusb-dev libdbus-1-dev libglib2.0-dev libudev-dev libical-dev libreadline-dev libdbus-glib-1-dev libbluetooth-dev
```

そして、本体のインストールですが、
BlueZは上記のようなパッケージが無いようなので、以下の手順でソースを落としてきてビルドします。

```bash:BlueZのビルド
$ wget http://www.kernel.org/pub/linux/bluetooth/bluez-5.50.tar.xz
$ xz -dv bluez-5.50.tar.xz
$ tar -xf bluez-5.50.tar
$ cd bluez-5.50/
$ ./configure --enable-experimental
$ make
// makeが長いのでしばらく待ちます・・・
$ sudo make install
```

以上で準備完了

#PythonでiBeaconを受信する
bluezを使ってiBeaconのアドバタイズパケットを受信するコードを書きます。
bluezのAPIは手順が難しくて説明サイトもないのでコードはおまじない状態です。
ここは思考停止して`interfaces_added()`、`properties_changed()`の中を実装していくことにします。

####注意
このソースでimportしている`bluezutils`はさっきインストールしたbluezの`test`フォルダに置いてある`bluezutils.py`を使いましょう。

```python:iBeaconのアドバタイズパケットを受信する
#!/usr/bin/python
# -*- coding: utf-8 -*-

import dbus
import dbus.mainloop.glib
try:
  from gi.repository import GObject
except ImportError:
  import gobject as GObject
import bluezutils

devices = {}

// アドバタイズパケット受信したときの処理
def interfaces_added(path, interfaces):
        properties = interfaces["org.bluez.Device1"]

        if not properties:
                return

        if path in devices:
                devices[path] = dict(devices[path].items() + properties.items())
        else:
                devices[path] = properties

        // ここら辺で自分がやりたい処理を書く
        // devies[path]オブジェクトがアドバタイズパケット

// アドバタイズパケット受信したときの処理
def properties_changed(interface, changed, invalidated, path):
        if interface != "org.bluez.Device1":
                return

        if path in devices:
                devices[path] = dict(devices[path].items() + changed.items())
        else:
                devices[path] = changed

        // ここら辺で自分がやりたい処理を書く
        // devies[path]オブジェクトがアドバタイズパケット

if __name__ == '__main__':
        dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)

        bus = dbus.SystemBus()
        adapter = bluezutils.find_adapter()


        bus.add_signal_receiver(interfaces_added,
                        dbus_interface = "org.freedesktop.DBus.ObjectManager",
                        signal_name = "InterfacesAdded")

        bus.add_signal_receiver(properties_changed,
                        dbus_interface = "org.freedesktop.DBus.Properties",
                        signal_name = "PropertiesChanged",
                        arg0 = "org.bluez.Device1",
                        path_keyword = "path")

        om = dbus.Interface(bus.get_object("org.bluez", "/"), "org.freedesktop.DBus.ObjectManager")
        objects = om.GetManagedObjects()

        for path, interfaces in objects.iteritems():
                if "org.bluez.Device1" in interfaces:
                        devices[path] = interfaces["org.bluez.Device1"]

        scan_filter = dict()
        scan_filter.update({ "Transport": "le" })
        adapter.SetDiscoveryFilter(scan_filter)
        adapter.StartDiscovery()

        mainloop = GObject.MainLoop()
        mainloop.run()
```

#アドバタイズパケットってなに？
さっきのサンプルで`interfaces_added(),properties_changed()`でのdevies[path]オブジェクトがアドバタイズパケットって事なんですが、これが何か、という話です。

このdevies[path]オブジェクトはdict型でいわゆるディクショナリーです。
詳細資料がないのでよくわからず、わかるところだけ埋めています。
**ManufacturerData**が今回重要なデータです。

| key | 説明 |
|:-:|:--|
|AddressType   |?   |
|Paired   |?   |
|ServicesResolved   |?   |
|Adapter   |?   |
|Alias   |?   |
|ManufacturerData|製造者固有データ.iBeaconの詳細情報が詰まっている|
|Connected   |?   |
|UUIDs   |?   |
|Address|BLEアドレス|
|RSSI|RawSignalStrengthInDBm.シグナル強度、いわゆるRSSI。数値が大きいほどシグナルが強い、近くから発信されている、ということ|
|Trusted   |?   |
|Blocked   |?   |

```python:iBeaconをスキャンしたときの処理
def scan_ibeacon(properties):
        address = "<unknown>"
        manufac = 0

        # AddressとManufacturerDataをGETする
        for key, value in properties.iteritems():
                if type(value) is dbus.String:
                        value = unicode(value).encode('ascii', 'replace')
                if (key == "Address"):
                        address = value
                if (key == "ManufacturerData"):
                        manufac = value

        # print("address = %" % (address))

        if type(manufac) is dbus.Dictionary:
                // manufac 中身をチェックする
```

#ManufacturerDataってなに？
iBeaconはManufacturerDataに詳細情報を入れることになっているらしく、バイト配列で、MAMORIOの個体識別IDなどが入っています。
以下のフォーマットです。

|index|size|説明|
|:-:|:-:|:--|
|0-1|2|Bluetooth SIGが企業に発行した識別子|
|2-18|16|UUID|
|18-19|2|Major.unsingned short|
|20-21|2|Minor.unsingned short|
|22|1|Measured Power.signed 8-bit integer|
|23|1|?|

#####UUID
- 16バイトの識別子
- MAMORIOの場合は`b9407f30-f5f8-466e-aff925556b57fe6e`

#####Major,Minor
- MAMORIOの個体識別ID
- unsingned shortでラズビアンはリトルエンディアンなので、変換の際は注意しましょう。

というわけなので**UUID,Major,MinorでMAMORIOを特定する**ことができます。

```python:ManufacturerDataデータをパースする
def parse_manufacture(mdata):
        uuid = ""
        major = 0
        minor = 0
        if( type(mdata) is dbus.Array and len(mdata) > 21):
                # UUID
                for item in mdata[2:6]:
                        uuid = uuid + format(item,'02x')
                uuid = uuid + "-"
                for item in mdata[6:8]:
                        uuid = uuid + format(item,'02x')
                uuid = uuid + "-"
                for item in mdata[8:10]:
                        uuid = uuid + format(item,'02x')
                uuid = uuid + "-"
                for item in mdata[10:18]:
                        uuid = uuid + format(item,'02x')

                # Major
                # - unsingned int(2バイト)
                # - リトルエンディアン
                majortmp = bytearray([int(mdata[19]),int(mdata[18])])
                major = struct.unpack('H',majortmp)[0]

                # Minor
                # - unsingned int(2バイト)
                # - リトルエンディアン
                minortmp = bytearray([int(mdata[21]),int(mdata[20])])
                minor = struct.unpack('H',minortmp)[0]
        return uuid,major,minor
```

#特定のMAMORIOを検知したらLINE通知をする
ここまでで
- LINE通知
- 特定のMAMORIOの検知
ができるようになりました。
さっきのソースをちょっと改造するだけです。

<details><summary>特定のMAMORIを検知したらLINE通知をする</summary><div>

```python:特定のMAMORIを検知したらLINE通知をする

MAMORIO_UUID = "b9407f30-f5f8-466e-aff925556b57fe6e"

# target [0]name,[1]major,[2]minor,[3]linetoken
MAMORIO_TARGETS = {
        ("check",11111,22222,""),
        ("gebo", 33333,44444,"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"),
        ("test", 55555,66666,"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy")
        }
        
def scan_ibeacon(properties):
        address = "<unknown>"
        manufac = 0

        # AddressとManufacturerDataをGETする
        for key, value in properties.iteritems():
                if type(value) is dbus.String:
                        value = unicode(value).encode('ascii', 'replace')
                if (key == "Address"):
                        address = value
                if (key == "ManufacturerData"):
                        manufac = value

        # print("address = %" % (address))

        if type(manufac) is dbus.Dictionary:
        
                # 特定のMAMORIOかどうかをチェックする
                uuid = ""
                major = 0
                minor = 0
                for mdata in manufac.values():
                        uuid,major,minor = parse_manufacture(mdata)

                if( uuid.lower() == MAMORIO_UUID.lower()):
                        #print("    MAMORIO! - uuid = %s" % uuid)
                        #print("    - major = %d , minor = %d" % (major,minor))

                        # ターゲットを検索
                        username = ""
                        token = ""
                        founds = [i for i in MAMORIO_TARGETS if i[1]==major and i[2]==minor]
                        if(len(founds) <= 0):
                                return
                        username = founds[0][0]
                        token = founds[0][3]

                        print("%s - major = %d , minor = %d -> user = %s" % (datetime.now(),major,minor,username))
                        
                        # LINE通知する
                        access_token = token
                        url = "https://notify-api.line.me/api/notify"
                        headers = {'Authorization': 'Bearer ' + access_token}
                        message = username
                        payload = {'message': message}
                        requests.post(url, headers=headers, params=payload,)                        
```

</div></details>

#細かい考慮
ここまでだとのアドバタイズパケットを検知するたびにLINE通知が来て困りますね。
いったん通知したMAMORIOの個体識別IDをメモリに保存して再通知しないようにしましょう。
そうすると、LINE通知が1回来たら二度とこなくなる・・・困った。
要するに、**MAMORIOが無くなったことを検知する必要があります**。
定期タイマで保存されているMAMORIOの個体識別IDをチェックして1分間アドバタイズパケットが来ていなかったら削除します。
ここの実装ですが、もう一個MAMORIOがあるんで、これをラズパイに貼りつけておいて定期タイマの代わりにして消込処理をしました（もっと賢いやり方があるね普通は）。

```python:検知したMAMORIOが60秒更新されていなければ削除する
def delete_targets():
        datenow = datetime.now()
        delete_username =""
        for key, value in targets.iteritems():
                span = datenow - value
                #print(span)
                if(span.seconds>60):
                        delete_username=key
                        break

        if( len(delete_username)>0):                        
                targets.pop(delete_username)
                print("delete %s - span = %s " % (delete_username,span))

        return delete_username
```

#実行してみる
そんなこんなで以下のような実装になりました。

- ラズパイに げぼ が近寄ってきたら（gebo用のMAMORIOが近づいてきたら）LINE通知する
- ラズパイから げぼ が離れたら（gebo用のMAMORIOが離れて60秒たったら）LINE通知する

<img width="310" alt="IMG_6455_p.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252291/7e52d71c-51cb-fbbe-694d-a23c10653c11.png">

**サンプルコードをまとめたものは[コチラ](https://github.com/gebogebogebo/notify_ibeacon/blob/master/source/notify_ibeacon.py)**
https://github.com/gebogebogebo/notify_ibeacon/blob/master/source/notify_ibeacon.py

#おつかれさまでした
夏休みの課題でした、ラズパイは面白い。

以下のサイトは大変参考になりました。

- [RaspberryPi同士でbluetooth通信](https://qiita.com/shippokun/items/0953160607833077163f)
- [Raspberry PiでiBeaconを検知する](https://qiita.com/katsuyoshi/items/9d5417495a47c4b15ac1)
- [BluezをD-busから使う LEデバイスをスキャンする](https://qiita.com/eggman/items/54976572283114c992ac)