iBeacon/Android/Firestoreで無料見守りサービスを作ってみた

# やりたいこと

* Firestoreを使ってタダで使える簡易見守りサービスを作る
  * 適当なiBeaconを子供にもたせておくと、家に着いたときに教えてくれる
  * 家の中で子供が活動すると、それをリアルタイムで通知してくれる
       * 活動の度合いは、ビーコンの受信信号強度（RSSI）の変化量で
* とりあえず初期プロトとして、必要最低限の機能を実装してみる

# 仕組み
![image.png](https://qiita-image-store.s3.amazonaws.com/0/102681/19da094f-b09f-d148-1dfb-5d2580ed1556.png)

* Androidアプリ がビーコンを受信すると受信データをFirestoreにAdd
* Androidアプリ、Webアプリがそれを受信し、UIを描画

# Androidアプリ
## iBeacon検知
iBeaconを検知して、そのデータをFirestoreに保存します。
おそらくiBeacon監視のデファクトなAltBeaconでやります。
https://qiita.com/kitaharamikiya/items/1abea6b39e85c8413b96

* ビルドし、Androidの設定で、アプリの位置情報のパーミッションをONにする
* 起動時にすでにBeaconがRegionの中の場合もケアしておく

```kotlin:MainActivity.kt

            override fun didDetermineStateForRegion(i: Int, region: Region) {
                when (i) {
                    INSIDE -> {
                        mBeaconManager.startRangingBeaconsInRegion(mRegion)
                    }
                }
            }
```

## Android で Firestoreを使う

つづいて保存部分です。
AndroidStudioのFirebaseのアシスタント機能を使ってやりましたが、ハマらずできました。これ便利ですね。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/102681/d131bea2-78d8-9e4e-09ff-e3268fd24406.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/102681/f0c71a03-b640-37ec-6f2b-7cadfa0c40d3.png)

このアシスタントに沿って行くと難なくセットアップ完了。
続いて、Beaconを受信したら、そのままFirestoreにaddするようにする。
Beacon情報に加えて、あとでクエリするために時刻情報も入れておく。

```kotlin
        mBeaconManager.addRangeNotifier { beacons, region ->
            beacons
                    .map {
                        val iBeacon = HashMap<String, Any>()
                        iBeacon["uuid"] = it.id1.toString()
                        iBeacon["major"] = it.id2.toInt()
                        iBeacon["minor"] = it.id3.toInt()
                        iBeacon["rssi"] = it.rssi
                        iBeacon["distance"] = it.distance
                        iBeacon["txPower"] = it.txPower
                        iBeacon["time"] = FieldValue.serverTimestamp()
                        iBeacon
                    }
                    .forEach {
                        db.collection("ibeacon")
                                .add(it)
                                .addOnSuccessListener { documentReference ->
                                    Log.d(TAG, "DocumentSnapshot added with ID: " + documentReference.id)
                                }
                                .addOnFailureListener { e ->
                                    Log.w(TAG, "Error adding document", e)
                                }
                    }

        }

```

これでビルドし、実行すると、BeaconがそのままドバドバFirestoreに保存されますｗ
![image.png](https://qiita-image-store.s3.amazonaws.com/0/102681/a968fd22-0bb1-3ce9-30a7-c3168adeeeb2.png)

が、数分放置しただけで相当なデータ量になってしまいました。。
こうなると無料枠で収まっているのか気になります（汗）　てことで、

![image.png](https://qiita-image-store.s3.amazonaws.com/0/102681/59778349-0db0-3f70-a1ed-6c6dde772867.png)
ここから、Quoats のページへとぶ
![image.png](https://qiita-image-store.s3.amazonaws.com/0/102681/8abbc8a8-23ed-f956-c42d-15edfb900491.png)
見ると `Cloud Firestore Read Operations` が５% になっている。一応まだ大丈夫だが、放っておくとヤバそうです。全てを送るのをやめて、ちょっとエッジ側でも処理するように検討します。

## 保存するデータ量を減らす（まとめる）

さすがに全てのパケットを上げると辛そうなので、１０秒間隔でRSSIの標準偏差を計算してStoreするように変更します。

* Kotlin での標準偏差の求め方
  * https://teratail.com/questions/80055

IBeaconをデータクラスにもっていく

```kotlin:IBeacon.kt
package iida.work.actiview

data class IBeacon(val uuid: String, val major: Int, val minor: Int, val rssi: Int, val distance: Double, val txPower: Int, val time: Long) {
}

```

```kotlin:MainActivity.kt
        mBeaconManager.addRangeNotifier { beacons, region ->

            beacons
                    .map {
                        IBeacon(it.id1.toString(), it.id2.toInt(), it.id3.toInt(), it.rssi, it.distance, it.txPower, System.currentTimeMillis())
                    }
                    .also {
                        Log.d(TAG, it.toString())
                    }
                    .forEach {
                        iBeacons.add(it)
                    }
                    .takeIf {
                        isBufferFilled()
                    }
                    ?.let {
                        createSendData(iBeacons[0].uuid, ActiveState.DWELL, sd(iBeacons))
                    }
                    ?.also {
                        iBeacons.clear()
                        addToDB(it)
                    }
        }
```

`isBufferFilled()` で10秒分のPacketをバッファリングして、溜まったら `sd()` で標準偏差を計算し、 `createSendData()` でFirestoreにAddする流れ。それぞれの関数はこんな感じで：

```kotlin:MainActivity.kt

    private fun createSendData(uuid: String, state: ActiveState, sd: Double) =
            HashMap<String, Any>()
                    .let {
                        it["uuid"] = uuid
                        it["time"] = FieldValue.serverTimestamp()
                        it["state"] = state.toString()
                        it["sd"] = sd
                        it
                    }

    private fun addToDB(it: HashMap<String, Any>) {
        db.collection("ibeacon")
                .add(it)
                .addOnSuccessListener { documentReference ->
                    Log.d(TAG, "DocumentSnapshot added with ID: " + documentReference.id)
                }
                .addOnFailureListener { e ->
                    Log.w(TAG, "Error adding document", e)
                }
    }

    private fun isBufferFilled(): Boolean = iBeacons.size > 0 && iBeacons[iBeacons.size - 1].time - iBeacons[0].time > 10000

    private fun sd(numArray: MutableList<IBeacon>): Double {
        val doubleArray = numArray.map { it.rssi }
        val mean = doubleArray.average()
        val sd = doubleArray.fold(0.0) { accumulator, next -> accumulator + Math.pow(next - mean, 2.0) }
        return Math.sqrt(sd / doubleArray.size)
    }

```

`TARGET_UUID` には、自分が使いたいBeaconのUUIDを入れました。 Androidアプリ `nRF Connect` とかで取得できます。今回は、Qrio Smart Tagを使用。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/102681/fb9dc649-53ad-718d-a156-03426c971b9f.png)

こんな感じで取れています。

## データの変更をうけてUIを更新

最後に、firestore の変更を検知してUIを更新する部分を書きます。
標準偏差の値に応じて、テキストと背景色を変更します。標準偏差の値が大きい（RSSIの変化が大きい）場合は活動量大、逆に小さい場合は活動量小とします。


```kotlin
    private fun startListenSnapshot() {
        db.collection("ibeacon")
                .orderBy("time", Query.Direction.ASCENDING)
                .addSnapshotListener { snapshot, e ->

                    if (e != null) {
                        Log.d(TAG, "Listen failed: $e")
                        return@addSnapshotListener
                    }
                    for (doc in snapshot.documentChanges) {
                        when (doc.type) {
                            DocumentChange.Type.ADDED -> {
                                Log.d(TAG, "ADDED: " + doc.document.data)
                                updateUI(doc.document.data)
                            }
                            else -> {
                                Log.d(TAG, "${doc.type}: " + doc.document.data)
                            }
                        }
                    }
                }
    }

    private fun updateUI(data: Map<String, Any>) {
        if (!data.containsKey("state")) return
        when (ActiveState.valueOf(data["state"].toString())) {
            ActiveState.ENTER -> {
                Toast.makeText(this, "${name}が家に帰りました。", Toast.LENGTH_SHORT).show()
                imageView.setBackgroundColor(Color.GREEN)
                textViewState.text = "いま帰宅しました"
            }
            ActiveState.DWELL -> {
                draw(data["sd"].toString().toDouble())
            }
            ActiveState.EXIT -> {
                Toast.makeText(this, "${name}が外出しました。", Toast.LENGTH_SHORT).show()
                imageView.setBackgroundColor(Color.GRAY)
                textViewState.text = "外出中"
            }
        }
    }

```
`startListenSnapshot` を `onCreate()` で呼んでます。 `${name}` にはビーコンを持つ子供の名前などを入れます。

これでざっくり完成です。このアプリを複数のAndroidに入れて、一つを自宅、もう一つを持ち歩けば、どこでも子供の活動がちょっとだけわかります。といっても、プロトタイプレベルなので、ちゃんと見守りたい人は、Qrioただいまキット( https://qrio.me/tadaima-kit/ ) などが良いかと思います。

ソースコードは後ほどGithubに公開予定です。ここからさらに、Push通知機能を入れたり、iBeaconの信号から活動を判定する部分をMLなどを活用して精度があげられると、より実用的になりそうです。

あと、そのうちWebアプリ側についても書いてみます。