MODE JAPAN Blog

IoTを活用したデジタルトランスフォーメーションのための情報、開発の現場から技術的な情報や取り組みについてお届けします。

Kotlin coroutines Flowを用いたAndroid SensorEventに対する非同期ストリーム処理

こんにちはソフトウェアエンジニアの @banana-umai です。

今回のエントリーではKotlinを用いたAndroid開発にまつわるTipsをご紹介したいと思います。

MODEではこれまでAndroidデバイスを用いて自動車の走行データを取得するためのAndroidアプリや、お客様ご自身でAndroidアプリケーションをMODE Cloudに接続してIoTゲートウェイとして利用するためのSDKを開発してきました。本稿では、MODEのAndroid開発において多く用いているセンサーデータを非同期ストリームとして処理する、という方法をご紹介します。具体的にはKotlinのcouroutinesライブラリを用いることになるのですが、その具体的な方法や、それによるメリットなどについてお伝えできればと思います。

Android開発をしている中でFlowやChannelなどの使い所はどこだろう?と思っている方々や、これからセンサーを用いた開発を行うとしている方々の参考になれば幸いです。

AndroidでSensor Dataを利用する方法

本題に入る前に、まずは基本的的なAndroidにおけるSensor Dataの利用方法について触れたいと思います。詳細は公式ドキュメントなどを参考にしていただければと思いますが、とても単純化すると以下のような形で利用することになると思います。

val listener = object : SensorEventListener {
        override fun onSensorChanged(event: SensorEvent?) { .... }
        override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {...}
}
val sensor = sensorManager.getDefaultSensor(SENSOR_TYPE)
sensorManager.registerListener(listener, sensor, ...)

SensorEventListenerを定義し、SensorManagerに登録することで、onSensorChangedメソッドがコールバックされてセンサーデータを受け取ることになります。Android開発においてコールバックベースのAPIは数多くあり、センサーデータの利用方法についても珍しい方法ではないと思いますが、一般的にコールバックベースのAPIを多用していくと処理が複雑化してきた際に制御の流れをコードから追いかけるのが難しくなりがちです。そこで、次節ではcoroutines.Flowを用いてコールバックベースのAPIを、ストリームベースのAPIに変換する方法を紹介します。

coroutines Flowを用いてセンサーデータをストリームとして扱う

FlowはKotlinにおける並行処理、非同期処理のデファクト標準になりつつある(なっている?)kotlinx.coroutinesライブラリの一部で、非同期データストリームを提供するコンポーネントです(詳細な説明は公式ドキュメントや作者の一人であるRoman Elizarov氏のブログなどを御覧ください)。

それでは、具体的にSensorEventストリームを作成する方法を説明していきたいと思います。ここでは、以下のように利用できるsensorEventFlow関数を作成します。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...snip
        lifecycleScope.launch {
            val sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
            val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) // 本当はsensorが利用可能かチェックしたほうが良い
            val delay = SensorManager.SENSOR_DELAY_NORMAL
            val handler = Handler(mainLooper) // 本当はBackground用のHandlerThreadを作ったほうが良い
            sensorEventFlow(sensor, sensorManager, delay, handler).collect { event ->
                Log.d("SensorEvent", "receive $event")
            }
        }
    }
}

細かいエラー処理などを端折るとsensorEventFlow関数の実装は概ね以下のような形になります。

@ExperimentalCoroutinesApi
fun sensorEventFlow(sensor: Sensor, manager: SensorManager, delay: Int, handler: Handler): Flow<SensorEvent> {
    return channelFlow<SensorEvent> { // ・・・1
        val listener = object : SensorEventListener {
            override fun onSensorChanged(event: SensorEvent?) {
                if (event?.sensor?.type == sensor.type) {
                    launch { send(event) } // ・・・2
                }
            }
            override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
                // do something
            }
        }
        try {
            manager.registerListener(listener, sensor, delay, handler)
            awaitClose() // ・・・3
        } finally {
            manager.unregisterListener(listener, sensor) // ・・・4
        }
    }
}
  1. channelFlowはFlow buildersと呼ばれる関数の一つで、引数となるコールバック関数内でストリームのデータを作成します。実際にやっているのは前節で解説したAndroidのSensor Dataの利用方法と同様にListnerを作成して、SensorManagerに登録しています。ポイントとなるのは、ListenerのonSensorChanged関数の実装です。
  2. send関数の呼び出しによって、ストリームにデータ(sensorEventFlowの場合はSensorEventオブジェクト)を送っています。send関数はsuspend関数なので、launch関数(Coroutine builder)によってcoroutineをinvokeする必要があります。
  3. awaitClose関数は、親となるCoroutineがCloseするまで、channelFlowに渡せれる関数の実行をブロックする処理です。この関数で処理をブロックしないと、即channelFlowのコールバック関数は終了してしまいlistenerがイベントを送信することができません。
  4. 最後にfinally句の中でsensorの監視を終了します。3)で書いたawaitClose関数はCoroutineが終了する際に、CancelationExceptionを発生させて処理を終了するため、finally句内の処理は実行することができます。

コールバックベースのAPIをストリームに変換することができれば、ストリームに対しての処理を書いていくことができるようになります。例えば以下のような形です。

sensorEventFlow(sensor, sensorManager, delay, handler)
  .buffer(100)
  .map { //データ変換処理 }
  .catch { //例外エラーハンドリング}
  .collect { // 最終的なデータの処理 }
  • buffer: 後続の処理で一時的なデータの消費が一時的に遅くなった際にデータを貯めこめるようにする
  • map: データの変換をする。例えば生のSensorEventをよりアプリケーションで利用したい抽象データ型に変換するなど
  • catch: 前段(upstream)の処理において何らかのエラー(例外)が発生することが予想される場合にエラー処理を挟む

この他にも様々なオペレータが定義されているので必要に応じて組み合わせて使うことができます。また、ここに定義されていないオペレータを自作することも可能です例: データのスロットリング

また、Flowの特徴の一つはバックプレッシャーが組み込まれている点です。バックプレッシャーとはストリーム処理において、ストリームの下流の処理の滞りを上流に伝えるための仕組みです*1。上記のオペレーターの組み合わせの例でbufferを定義していますが、場合によってはそのbufferサイズを超えてしまうことがあるかもしれません。最終的にこのバックプレッシャーはストリームデータの発生元まで返ってくることになります。SensorEventListnerのonSensorChangedメソッド内でsendを行うところまで戻ってきます。バックプレッシャーが発生した場合に、sendはデフォルトでブロックしますが、coroutineで提供されているselect APIを用いることで、バックプレッシャーが生じた際に何らかの処理を行うように定義することも可能です。

fun sensorEventFlow(sensor: Sensor, manager: SensorManager, delay: Int, handler: Handler): Flow<SensorEvent> {
    return channelFlow<SensorEvent> {
        val side = Channel<SensorEvent>()
        launch {
            for (event in side) {
                // do something here
            }
        }
        val listener = object : SensorEventListener {
            override fun onSensorChanged(event: SensorEvent?) {
                if (event?.sensor?.type == sensor.type) {
                    launch {
                        select<Unit> {
                            onSend(event) {}
                            side.onSend(event) {}
                        }
                    }
                }
            }
            override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
                // do something
            }
        }
        try {
            manager.registerListener(listener, sensor, delay, handler)
            awaitClose()
        } finally {
            side.close()
            manager.unregisterListener(listener, sensor)
        }
    }
}

上記のコードでは、SensorEventストリームの下流の処理で詰まってバックプレッシャーが大本まで到達した際に、select式を使って、sideチャネルに処理を逃がすための処理です。sideチャネルの宣言した直後にcoroutineを作成し、その中でsideチャネルに入ってきた場合の処理を記述します。ログを取る、ユーザーに通知する、sensorの監視を停止する、再起動する、など、アプリケーションの性質に応じてどのような処理するのが適切かは変わってくるかと思われます。

まとめ

channelFlowというFlow builder関数を使うことで、コールバックベースのAndroidのセンサーAPIをストリームとして扱うことができるようになります。coroutinesライブラリにはストリームに対する処理を行うためのツールキットが揃っており、宣言的にデータの加工、フィルタリング、エラー処理などを施すことができるというメリットがあります。また、Flowはバックプレッシャーが組み込まれているため、ストリーム処理の下流における処理の不具合を上流に伝達し、ハンドルすることが可能です。なお、Javaをベースで実装する場合、概ね同様のことをRxJavaのFlowableを用いて実現することが可能ですので、別の機会にご紹介できればと思います。センサーはソフトウェア的に時系列で状態をデータを発生するデータソースとみなせるため、ストリーム処理とは非常に相性が良いと思われます。また、Androidとセンサーデータを用いた(ビジネス)アプリケーションの開発に興味がある方の参考になれば幸いです。

最後に、センサーを活用したコネクティッドなビジネスアプリケーションを構築したいという方におかれましては、MODEではAndroidのSDKをはじめ、MODE Labという共同開発方式や各種ソリューションを提供していますので、ご興味ある方はぜひお問い合わせください。

www.tinkermode.jp

*1:バックプレッシャーの詳細な説明はBackpressure explained — the resisted flow of data through softwareがわかりやすいかと思います

MODE 時系列データベースの使い方 (Collection 編)

エンジニアの野本です。先日時系列データベース (以下 TSDB) の新バージョンリリースをお知らせしました。

「MODE Core Platform」における 時系列データベースの新バージョンリリースのお知らせ

主なアップデートとして「多次元データ対応」あげられています。これは Collection という機能として提供しているもので、この記事中でもあるように次元数の多いデータを今までより簡単に扱えるようにする機能です。 今回は Collection がどのようなものか、どのように使えるのかを簡単に紹介します。

Collection で何が変わったの?

以前のブログ記事で TSDB の基本的な使い方を紹介しました。 今までは時系列データを 1 次元でのみ扱え、Series ID をキーとして 1 データポイント当たり 1 つの値のみ保持することが可能でした。

Series ID 日時
series-01 2020/06/01 00:00:42 4.88
series-01 2020/06/01 00:01:42 4.94
series-01 2020/06/01 00:02:42 4.94
series-02 2020/04/22 10:00:30 60
series-02 2020/04/22 10:02:22 -24

これが Collection により、1 データポイントが Collection として管理され、複数の値を保持出来るようになりました。

Collection ID: collection-01

日時 値:v1 値:v2 値:v3 タグ:version タグ:ID
2020/06/01 00:00:42 4.88 24 1 V1 0001
2020/06/01 00:01:42 4.94 12 0 V1 0002
2020/06/01 00:02:42 4.94 35 1 V2 1001

Collection ID: collection-02

日時 値:temperature 値:co2 タグ:density
2020/06/01 00:00:42 30.88 800.2 normal
2020/06/01 00:01:42 29.94 880.2 normal
2020/06/01 00:02:42 28.94 1400.4 high

主な違いとして以下が上げられます

  • Series ID に変わり Collection ID でデータの集合を管理
  • 各 Collection ごとに値の数や値の名称を設定出来る
  • データポイントに対し付加情報をタグとして付与出来る
    • データ参照時にタグ情報も取得することで、取得したデータをタグで分類するようなことが可能
    • タグも値と同様 Collection ごとに可変で名称を設定可能

このように、データポイントに対し柔軟に複数のデータを格納することが出来るようになりました。XYZ 軸がある加速度センサーや緯度経度を持つ位置情報など次元数の多いセンサーのデータを同時に扱うことが簡単になることが分かると思います。

Collection データの取得方法

基本的には先に紹介した Series ID により管理されたデータとほぼ同じ方法で格納されたデータを取得出来ます。Collection 固有のパラメータやどのような形式でデータを取得出来るのかを紹介します。Collection の API も詳細は 公式ドキュメント に記載されているので参考にしてみて下さい。

Collection 一覧

現在格納されている Collection の一覧は以下のように確認出来ます。Collection に値やタグがどのようなキー名で保存されているかの情報も確認出来ます。

$ curl -X GET -H 'Authorization: ModeCloud [PROJECT_API_KEY]' https://api.tinkermode.com/homes/123/smartModules/tsdb/collections
[
  {
    "homeId": 123,
    "id": "collection1",
    "moduleId": "tsdb",
    "timeZone": "Asia/Tokyo",
    "tagNames": [
      "density",
    ],
    "valueNames": [
      "temperature",
      "co2"
    ]
  },
  {
    "homeId": 123,
    "id": "collection2",
    "moduleId": "tsdb",
    "timeZone": "Asia/Tokyo",
    "tagNames": [
      "tag1",
      "tag2",
      "tag3"
    ],
    "valueNames": [
      "v1",
      "v2",
      "v3",
      "v4"
    ]
  }
]

期間内データの取得

Collection ID をキーに期間指定してデータを取得します。期間指定や集約の種類の指定は Series データ取得時と同様です。Collection では 1 つのデータポイントに複数の値が格納されています。取得したい値の名称を selectValues パラメータにて指定します。複数の値を取得したい場合はカンマ区切リで指定します。期間による集約の解像度は Series データと同じです。

curl -X GET -H 'Authorization: ModeCloud [PROJECT_API_KEY]'  https://api.tinkermode.com/homes/123/smartModules/tsdb/collection/collection1/data\?begin\=2020-06-01T00:00:00.000Z\&end\=2020-06-30T00:59:59.999Z\&aggregation\=avg\&selectValues\=temperature,co2
{
  "aggregation": "avg",
  "begin": "2020-06-01T00:00:00Z",
  "collectionId": "collection1",
  "data": [
    [
      "2020-06-01T09:00:00+09:00",
      19.5,
      1289.7
    ],
    [
      "2020-06-01T09:00:15+09:00",
      18.7,
      849.2
    ],
    [
      "2020-06-01T09:01:15+09:00",
      17.3,
      1343.05
    ],
    ...
    [
      "2020-06-01T09:55:45+09:00",
      19.3,
      89.98
    ],
    [
      "2020-06-01T09:56:45+09:00",
      20.8,
      557.01
    ],
    [
      "2020-06-01T09:59:15+09:00",
      21.0,
      1503.12
    ]
  ],
  "end": "2020-06-01T00:59:59.999Z",
  "resolution": "15sec"
}

data キーの各データポイントの情報の配列です。各データポイントの情報も配列になっており、先頭がタイムスタンプ、以降は selectValues で指定した値が順番に入っています。

生のデータポイントの取得

Series データと同様、ts パラメータで開始日時を、 limit で取得最大件数を指定して生の Collection データを取得出来ます。期間による取得と同様 selectValues パラメータにて取得する値の設定が必要です。また、生のデータにはタグ情報も格納されているので selectTags パラメータにより取得したいタグ名を設定しタグの値も取得出来ます。

$ curl -X GET -H 'Authorization: ModeCloud [PROJECT_API_KEY]' https://api.tinkermode.com/homes/123/smartModules/tsdb/collections/collection1/data\?t\s=2020-06-01T00:00:00.000Z\&limit\=100\&selectValues\=temperature,co2\&selectTags\=density
{
  "collectionId": "collection1",
  "data": [
    [
      "2020-06-01T09:00:00+09:00",
      22.1,
      1881.01,
      "high"
    ],
    [
      "2020-06-01T09:00:09+09:00",
      17.0,
      698.57,
      "normal"
    ],
    [
      "2020-06-01T09:00:21+09:00",
      18.7,
      849.27,
      "normal"
    ],
    [
      "2020-06-01T09:01:16+09:00",
      19.3,
      1343.05,
      "high"
    ],
    [
      "2020-06-01T09:05:41+09:00",
      20.3,
      313.03,
      "normal"
    ],
    ...
  ],
  "limit": 100,
  "ts": "2020-06-01T00:00:00Z"

data キーの内容は期間指定のデータとほぼ同じで、selectTags で指定したタグ情報が値の次に格納されています。

データのエクスポート

Series データ同様 csv 形式でファイルをダウンロード出来ます。

$ payload=$(cat <<EOF
{
    "begin": "2020-04-01T00:00:00Z",
    "end": "2020-04-01T23:59:59Z",
    "collectionds": ["collection1"]
}
EOF)

$ curl -X POST -H 'Authorization: ModeCloud [PROJECT_API_KEY]' -d '${payload}' https://api.tinkermode.com/homes/123/smartModules/tsdb/export
2020/06/01 00:00:00,22.1,1881.01,high
2020/06/01 00:00:09,17.1,698.57,normal
2020/06/01 00:00:21,18.7,849.27,normal
2020/06/01 00:01:16,19.3,1343.05,high
2020/06/01 00:05:41,17.3,313.03,normal
2020/06/01 00:10:52,19.2,604.4,normal
2020/06/01 00:12:37,16.0,1030.42,high
2020/06/01 00:14:04,14.2,761.31,normal
...

まとめ

Collection として次元数の多いデータを扱えるようになったことにより今まで以上に幅広いセンサーや機器のデータを簡単に格納し、より柔軟に扱えるようになりました。ちなみに今までの 1 次元のデータも引き続き使うことが出来るので収集したいシンプルなセンサーは従来の方法で利用し続けたりデータの種類によって使い分けることが出来ます。 次元数の多いデータのセンサーのデータを同時にうまく扱ったシステムを検討している方は是非一度ご利用をご検討してみて下さい!

新 Sensor Cloud におけるロジックの取り扱いとコードの構造

本記事は、以前紹介した新 Sensor Cloud の実装紹介の続編となります。前回は型付けについて解説しましたが、今回はより大局的なコードの構造についてお話します。 blog.tinkermode.jp

MODE Platform API と Sensor Cloud の関係

本題に入る前に、MODE Platform API と Sensor Cloud の関係について説明しておきたいと思います。MODE Platform が Web API により提供しているのは時系列データベースやデバイス管理といった、IoT 開発において一般的に必要となる機能群です。Sensor Cloud を始めとするMODE の各種クラウドサービスは、この MODE Platform API による汎用の機能を土台とし、それを応用して各サービス固有の機能を実現しています。

f:id:matsushita-mode:20200703104500p:plain
MODE Platform API と各種 MODE のクラウドサービス

以前の新 Sensor Cloud に関する記事においてはこの API コールをしている部分の型付けついて解説しましたが、今回は Sensor Cloud 固有の機能のロジック (UI に依存しない部分) がどのようなコードの構造になっているのか、また前述の API コール部分とどのような関係になっているのかを解説していきます。このロジックは MODE Platform の API コールを駆使して実現されていますが、複数の観点を考慮して妥当なコードの形にしていく必要があります。

API コール部分について

まず前提としてこの API コール部分は、各 API と単射になるような関数の集りです。つまり 1 API につき 1 つの関数が実装されており、ただし一部の API については未対応という状況です。複数の API を呼び出すような関数は作っていませんし、もっと言うとレスポンスを加工するような処理も行なっていない、愚直なラッパライブラリといった感じになっています。実際は複数の API コールを組み合わせたり、レスポンスを加工して用いることが大半なのですが、このようにした理由は以下です。

  1. Sensor Cloud を超えて、将来的に MODE 共通の SDK としての切り出しを可能とする
  2. 既存の MODE Platform API のドキュメントがそのままコードドキュメントとして参照できる
  3. 変動性の低いコンポーネントの切り出し

1, 2 は、MODE Platform API を利用する JavaScript/TypeScript の開発を将来的に高速に行なえるようにするための仕込みです。MODE 社内の開発もそうですが、将来的に SDK を外部公開した場合には MODE を活用したサードパーティの開発も効率化が見込めます。

3 はメンテナンス性に関わる部分です。MODE Platform API は外部向けにも公開されている Web API であり仕様変更が非常に起きにくいです。 それらとストレートに対応するコンポーネントであれば、同程度に変更が起きにくくなります。したがって変更時には少なくともこれらのコンポーネントには手を入れる必要がなく、その呼び出し側だけを改修をすれば済むということになります。

Sensor Cloud 固有のロジック

さて、こうなってくると Sensor Cloud 固有のロジックは上記の API コール部分とは完全に分離した形で記述する必要が出てきます。また、こうしたロジックのテスタビリティとコードの見通しを考えると、View からは分離して記述すべきと思います。したがってこうした処理は logic.ts というファイルに切り出すこととしました。

少し脱線します。 この logic.ts と API コール層の関係は、クリーンアーキテクチャで言うところのエンティティとユースケースの関係にあると見ることもできるでしょうが、今回はクリーンアーキテクチャを意識してこの構造にしたわけではありません。前述の通り API コール層を分離したいことと、ロジックを View から分離したいことを考えて、自然とこのように落ち着きました。 加えて、依存方向は必ず View -> ロジック -> API コール層 となるように今回しましたが、これもクリーンアーキテクチャがどうというよりは依存が常に一定方向であることで複雑度を上げずに済むことを考えれば自然なことと思います。 Rich Hickey は Simple Made Easy の中で complect (複数のものの絡まり合い) が simplicity を損いメンテナンス性を下げる (意訳) と述べていますが、依存方向の complect も回避したいというのが個人的な考えです。

閑話休題。

さてこのロジック層ですが、ラフに機能単位でディレクトリ分けしています。以下がそのイメージです。

/
|
+- core/
|   |
|   +- api.ts
|
+- dashboard/
|   |
|   +- logic.ts
|   +- logic.test.ts
|   +- view 系コード
|
+- hardware/
|   |
|   +- logic.ts
|   +- logic.test.ts
|   +- view 系コード
:

core/api.ts が共通の API コール層です。 アプリケーションの複雑度によってはロジック層をさらに細かく分割・構成するべきかもしれませんが、今の Sensor Cloud で求められているものを考えればこうしてユーザ向けの機能で分割している程度で十分であり、現時点においてこれ以上は過剰になり設計と把握のコストが上回ると判断しました。

また logic.ts は基本的にクラスを使わず、関数 (+ データ構造) による構成をとっています。JavaScript/TypeScript は OOP を強制する言語ではないと考えていますし、規模的にクラスによるモデリングが冗長に感じられたためです。これにより logic.ts は自身のステートを持ちません。

ロジックのテストに関わる構造

さてテスタビリティも考慮して logic.ts を設けているわけなので、logic.test.ts について考えましょう。これはそのファイル名の通り logic.ts に対するテストとなります。 テストが書きにくく UI 改修の起きやすい view 層をさて置いて、Sensor Cloud 固有のロジックについてチェックをしたいわけです。

前節で述べた通り logic.ts は自身のステートを持たない関数の集まりです。ここで発生する副作用は以下であり、テストを書くにあたってはこれらの考慮が必要です。

  1. Web API コール
  2. Web Storage の利用
  3. console によるログ出力

2, 3 については対処が簡単です。JavaScript/TypeScript の特性として組込みオブジェクトのプロパティは簡単に書き換えられますし、テストフレームワークがそれを利用したテストダブルの機能を提供していたりします。今回は jest を使っているので jest.spyOn を利用しました。

さて Web API コール部分についてですが、結論から言うと axios-mock-adapter を利用しました。 当初は core/api.ts に interface を用意しておき、DI することで core/api.ts をモックに差し替えてのテストにすることを検討していたのですが、このためだけに DI を導入せずとも axios-mock-adapter で事足りると考えこの形に落ち着きました。

なお core/api.ts は axios に強く依存しているように感じるかと思いますが、axios 利用部分は core/api.ts 内部の private な関数として抽象化されているため、万が一 axios から別のライブラリへ乗り換えることになっても修正は局所的になります*1

まとめ

以上、新 Sensor Cloud のロジックに関連したコード構造について説明いたしました。将来の開発速度を犠牲にせず、かつ過剰にならないラインを自分なりに狙ってみたのですがいかがでしょうか。これから MODE のビジネスが発展していくにつれ Sensor Cloud の役割が変化していき適切な構造は変わっていく (あるいは今まで気づいていなかった不適切さが明らかになる) 可能性は当然あるでしょう。その時が来たら、得られた学びから設計・実装を考え直していければと思います。

*1:もちろん axios-mock-adapter を使ったテストコードは全面的に修正が発生しますが、トレードオフとして許容しています。axios の乗り換え自体が発生しにくいであろうと思われるので、Web API モック定義の修正くらいは受け入れます。

MODE 時系列データベースの使い方 (基本編)

こんにちは、エンジニアの野本です。今までの記事でも度々紹介してきましたが、MODE ではセンサーデータを MODE 内部で実装した時系列データベース (Time Series Database 以下 TSDB) に格納しています。Sensor Cloud で提供される Web UI にセンサーデータのグラフ表示がありますがこれらのデータは TSDB に格納されています。TSDB は HTTP REST API によりデータの格納/取得などが可能です。Sensor Cloud もこの REST API を利用して TSDB に格納されているデータを表示しています。

f:id:reprimande:20200630180317p:plain

今回はこれら格納されているデータをどのように取得出来るかを紹介してみます。これを利用することによりユーザは TSDB のデータを利用した独自のアプリケーションを開発することも出来ます。

データ取得の基本

Series ID

各センサーデータの値は Series ID と呼ばれるユニークな ID をキーとして登録されています。Series ID は基本的には Sensor Cloud であれば Sensor Gateway によりセンサーの種類などに基づいて自動的に採番されます。TSDB に格納されている Series ID 一覧は以下のクエリにより確認出来ます。この Series ID ごとに TSDB に格納されているデータを取得することになります。

$ curl -X GET -H 'Authorization: ModeCloud [PROJECT_API_KEY]' https://api.tinkermode.com/homes/123/smartModules/tsdb/timeSeries
[
  {
    "homeId": 123,
    "id": "0001:01234567-value:0",
    "moduleId": "tsdb",
    "timeZone": "UTC"
  },
  {
    "homeId": 123,
    "id": "0001:01234567-temperature:0",
    "moduleId": "tsdb",
    "timeZone": "UTC"
  },
  {
    "homeId": 123,
    "id": "0001:01234567-co2:0",
    "moduleId": "tsdb",
    "timeZone": "UTC"
  }
]

期間内データの取得

TSDB からのデータ取得は対象の Series ID に対し期間指定をしての取得が基本になります。クエリパラメータの beginend により期間指定を行います。

curl -X GET -H 'Authorization: ModeCloud [PROJECT_API_KEY]'  https://api.tinkermode.com/homes/123/smartModules/tsdb/timeSeries/0001:01234567-temperature:0/data\?begin\=2020-04-01T00:00:00.000Z\&end\=2020-04-30T23:59:59.999Z\&aggregation\=avg
{
  "aggregation": "avg",
  "begin": "2020-04-01T00:00:00Z",
  "data": [
    [
      "2020-04-01T00:00:00Z",
      5.150430555555556
    ],
    [
      "2020-04-02T00:00:00Z",
      5.131888888888889
    ],
    [
      "2020-04-03T00:00:00Z",
      5.163805555555555
    ],
...
    [
      "2020-04-28T00:00:00Z",
      5.746069444444443
    ],
    [
      "2020-04-29T00:00:00Z",
      5.507174825174825
    ],
    [
      "2020-04-30T00:00:00Z",
      5.217708333333333
    ]
  ],
  "end": "2020-04-30T23:59:59.999Z",
  "resolution": "1day",
  "seriesId": "0001:01234567-temperature:0"
}

ここで resolution1day となっていますが、先の beginend で指定した期間の範囲によって取得する際のデータの解像度が自動的に決まります。データを取得するごとに期間内のデータを全て集め集計していると期間によっては膨大な時間がかかってしまうこともあります。MODE の TSDB では期間に対し適切な解像度にデータを集約し保存しており、広い期間であっても高速に集約データを取り出すことが出来ます。 期間と解像度の対応は以下のようになっています。

期間 解像度
5分以下 集約なし
5分 - 15分 5秒
15分 - 1時間 15秒
1時間 - 12時間 1分
12時間 - 5日 10分
5日 - 28日 1時間
28日 - 1年 1日
1年 - 5年 1週間
5年以上 1ヶ月

また、集約の種類は aggregation クエリパラメータにより指定出来ます。先の例では平均値 (avg) を指定しています。集約の種類は以下になります。

aggregation パラメータの値 集約の種類
avg 平均
max 最大値
min 最小値
sum 合計値
count データ数

生のデータポイントの取得

集約値ではなく生のデータポイントを参照したい場合は ts パラメータと limit パラメータを利用して取得することが出来ます。

$ curl -X GET -H 'Authorization: ModeCloud [PROJECT_API_KEY]' https://api.tinkermode.com/homes/123/smartModules/tsdb/timeSeries/0001:01234567-temperature:0/data\?ts\=2020-04-01T00:00:00.000Z\&limit\=100
{
  "data": [
    [
      "2020-04-01T00:00:42.733Z",
      5.42
    ],
    [
      "2020-04-01T00:01:42.792Z",
      5.38
    ],
    [
      "2020-04-01T00:02:42.659Z",
      5.36
    ],
…
   [
      "2020-04-01T08:17:40.001Z",
      4.96
    ],
    [
      "2020-04-01T08:18:39.866Z",
      4.94
    ],
    [
      "2020-04-01T08:19:40.12Z",
      4.96
    ]
  ],
  "limit": 100,
  "seriesId": "0001:01234567-temperature:0",
  "ts": "2020-04-01T00:00:00Z"
}

ts にセットされた日時を基準に limit に指定された値を最大数としてデータを取得します。limit は -500 から 500 の範囲で指定可能です。(なので取得出来るデータの最大数は 500 件です。)

格納データ期間

対象の Series ID に格納されているデータの期間を知りたい場合は以下のクエリで確認出来ます。

$ curl -X GET -H  'Authorization: ModeCloud [PROJECT_API_KEY]' https://api.tinkermode.com/homes/123/smartModules/tsdb/timeSeries/0001:01234567-temperature:0/timeRange
{
  "begin": "2017-07-27T02:38:20Z",
  "end": "2020-06-30T05:41:05.152Z",
  "seriesId": "0001:01234567-temperature:0"
}

データのエクスポート

任意の期間内のデータを一括で取得したい場合はファイルによるエクスポート機能を使います。 エクスポートを行いたい場合、まずエクスポートしたい期間と対象の Series ID を指定してエクスポートのリクエストを POST で発行します。

$ payload=$(cat <<EOF
{
    "begin": "2020-04-01T00:00:00Z",
    "end": "2020-04-01T23:59:59Z",
    "seriesIds": ["0001:01234567-temperature:0"]
}
EOF)

$ curl -X POST -H 'Authorization: ModeCloud [PROJECT_API_KEY]' -d '${payload}' https://api.tinkermode.com/homes/123/smartModules/tsdb/export

レスポンスとして以下情報が返却されます。

{
"dataUrl": "https://s3-us-west-2.amazonaws.com/scdata.tinkermode.com/export/123/XXXX.zip…",
"statusUrl": "https://s3-us-west-2.amazonaws.com/scdata.tinkermode.com/export/123/XXXX.json…"
}

dataUrl は最終的に生成されるエクスポートデータの URL です。ファイルは zip 形式になっています。statusUrl はエクスポート可能かどうかの状態が記述されている JSON ファイルの URL です。エクスポート処理はデータを収集しファイルを生成するのに時間がかかるので、この URL により完了したかどうかを確認出来ます。 ダウンロード出来る状態になると

{ "status": "SUCCESS" }

という内容になり、dataUrl からファイルをダウンロードすることが出来るようになります。 エクスポートしたファイルは以下のように日時と値が対となった csv 形式になっていています。

2020/06/01 00:00:42,4.88
2020/06/01 00:01:42,4.94
2020/06/01 00:02:42,4.94
2020/06/01 00:03:43,4.96
2020/06/01 00:04:42,4.98
2020/06/01 00:05:42,5
2020/06/01 00:06:42,5
2020/06/01 00:07:42,5.04
2020/06/01 00:08:42,5.06
2020/06/01 00:09:42,5.08
2020/06/01 00:10:42,5.12
2020/06/01 00:11:42,5.14
2020/06/01 00:12:42,5.14
...

まとめ

MODE の時系列データベースからデータを取り出す基本的な方法を紹介しました。必要に応じた形でデータを取り出すことが出来、また大量データでも扱いやすい解像度で高速にデータを取り出すことが出来ます。 以下ドキュメントで API の一覧を公開しています。データの格納や削除も API を利用して行えます。実際の利用時はこちらのドキュメントを参考に活用してみて下さい。

https://dev.tinkermode.com/platform/api/time-series-database

また、今回紹介した機能以外にも新たに追加されている便利な機能もあります。それらも今後紹介していきたいと思います。

また、MODE Labs ではこれらを利用した開発を弊社エンジニアのサポートとともに行えます。お気軽にご相談下さい!

www.tinkermode.jp

次期 MODE モビリティクラウドのご紹介

MODE ソフトウェアエンジニアの武田です。今回は現在開発を進めているMODE モビリティクラウドの次期バージョンについてご紹介します。

MODE モビリティクラウドはMODEが提供する領域特化型のサービスの一つであり、自動車や産業用車両といった移動体(モビリティ)からのデータ収集に特化した機能をSaaS型で提供しています。 MODE モビリティクラウドの概要についてはこちらの記事をご覧ください。

MODE モビリティクラウドのご紹介 - MODE JAPAN Blog

次期バージョンでは様々なお客さまの声をもとに、MaaSビジネスのIoT基盤としてより使いやすくなるよう内部を一新しました。

各機能は段階的にリリースを行っていきます。ここでは初期リリースにて追加される予定の機能や変更点をご紹介いたします。 詳細は今後変更される可能性がありますが、予めご了承ください。

f:id:mode_t:20200622220738j:plain

アセットトラッキングへの対応

車両だけではなく、より幅広いモノを一元的に管理できるようになります。

例えば物流業務の全体を考えた場合、輸送車両だけではなく、荷物やその倉庫、フォークリフト、トレーラーやコンテナ、パレットや台車といった物流資材などの情報も合わせて管理する必要があります。 今回、システム内部のデータ構造を見直し、車両だけではなく様々なモノ(アセットと呼びます)に関するデータを一元的に管理できるようにしました。

各アセットが持つ基本的なデータ構造として、位置情報や温度、湿度などの基本的なセンサーデータと「イベント」と呼ぶ時系列データがあります。 イベントとは、どのアセットに対して、いつ、どこで、誰が、何をした、というデータです。イベントはセンサーデータをもとに自動的に生成したりオペレーターの手動操作によって記録したりすることができます。 例えば、車両の場合は急ブレーキや急ハンドルを「危険運転」として管理できます。荷物や物流資材の場合は入庫や出荷、積み込み、到着といった「実績」を自動または手動で記録することができます。 収集したイベントはお客さまの社内基幹システムへリアルタイムで通知・連携することができますので、日々のビジネスフローの中でそのデータをリアルタイムに利用することができます。

なお、全てのアセットにMODEゲートウェイを設置する必要はありません。例えば、各アセットにバーコードやRFID、ビーコンといった何らかの識別子をつけておき、 それを各拠点に設置するMODEゲートウェイが読み込む構成を取ることも可能です。

オペレーターモニタリングへの対応

試験的な機能となりますが、オペレーターのモニタリング機能を追加します。

例えば、シートセンサーを活用したドライバーの体調(眠気・疲労)のデータのほか、各ドライバーの運行履歴や危険運転履歴などのデータを確認することができるようになります。 また、将来的には日々の体温やアルコール検査結果などの体調情報も合わせて管理し、日々の安全運転指導や健康管理にご利用していただくことを想定しています。

MODE Time-series Database (TSDB)への対応

従来から対応していたSDSに加え、TSDBにも標準で対応します。収集したセンサーデータをより簡単に利用することができるようになりました。

TSDBは通常のDBが苦手な時系列操作に特化したMODE独自のデータベースプロダクトです。 長期間のデータクエリでも瞬時にデータを返すことができ、センサーデータ収集と可視化に向いています。 SDSは高頻度/大容量データに対応したMODE独自のデータストアです。ミリ秒単位のセンサーデータといった大容量のデータや動画や写真といったバイナリデータの収集に向いています。 どちらもゲートウェイ機能と連携することで移動中の圏外など通信が不安定な環境でもデータを確実に収集する「データの到達保証」を実現しています。

次期 MODE モビリティクラウドの標準構成では、車両の位置情報や温度情報などグラフで可視化したいセンサーデータにはTSDBを使い、 動画やお客さまのデバイスが出力するバイナリファイルのアップロードにはSDSを用いています。

高いカスタマイズ性

内部設計とAPIを見直すことでゲートウェイ機能とクラウド機能が粗結合となりました。より柔軟な構成でシステムをご利用していただくことができます。

具体的には、MODE モビリティクラウドが提供するクラウド機能だけ、または逆に、ゲートウェイ機能だけをご利用いただくことができるようになります。 例えば、お客さまにて通信型ドラレコのようなデバイスをすでにお持ちの場合、当社が提供するSDKを組み込んでいただくことでクラウド機能だけをご利用いただくことができます。 または逆に、当社が提供するWebアプリケーションが不要な場合、ゲートウェイ機能とデータストア(TSDB、SDS)、データストアが提供するAPIだけを使うこともできます。 さらには、お客さまにてSDSやTSDBに相当するデータ基盤をお持ちの場合、ゲートウェイ機能だけを利用してデータをお客さまのクラウド基盤に直接蓄積することもできます。

その他の機能追加

その他、初回リリースでは以下の基本機能の追加を予定しております。

複数カメラ対応 (ゲートウェイ機能)

複数台のカメラの動画を同時に保存、管理することができます。台数にソフトウェア的な制限はありませんが電源容量や通信帯域といったハードウェア面の制約があります。 詳しくはお問い合わせください。

タブレット、スマートフォン対応 (クラウド機能)

Webアプリケーションを刷新し、タブレットやスマートフォンから利用しやすくなります。

危険運転箇所ヒートマップ表示(クラウド機能)

急ブレーキや急ハンドルなどの危険運転の多発箇所を地図上で確認できるようになります。

f:id:mode_t:20200622221452j:plain

標準では危険運転が多い箇所を赤く表示しますが、配送拠点周辺の混雑状況を地図上に可視化するなどカスタマイズしてご提供することもできます。

長期連続稼働対応(クラウド機能)

産業用車両やコンテナやトレーラーといったモノの場合、24時間365日連続して継続的にデータ収集を行い続ける必要があります。 UIを見直し、長期連続稼働した場合でもパフォーマンスが落ちること無くスムーズに操作ができるようになりました。

ファイルアップロード機能(ゲートウェイ機能)

お客さまのデバイスで生成されるデータファイルやログファイルをゲートウェイ機能を経由してクラウドにアップロードする機能です。 アップロードしたファイルはWebアプリケーションからダウンロードすることができます。また、AWSをお使いであれば、お客さまが管理するS3にアップロードすることもできます。 アップロードするファイルをMODE ゲートウェイが受け取る方法はお客さまのデバイスに合わせて個別対応いたします。ご相談ください。

まとめ

MODE モビリティクラウドは今回のバージョンアップにて、自動車だけではなくその周りの場所やモノ、それに関わる人のデータやイベントも一元的に収集・管理・可視化できるようになります。 また、デバイス・センサーからクラウドアプリケーションまですべてのレイヤーを包括的にカバーしたオールインワンパッケージではありますが、企業によって違うビジネスプロセスや他社との差別化に柔軟に対応できる高いカスタマイズ性を備えています。

これにより、お客様のビジネスに直結するIoT技術の活用とデジタルトランスフォーメーションを支援します。詳しくはお気軽にお問い合わせください。

www.tinkermode.jp

IoTクラウドとしてのMODEの紹介 〜 IoT サービス・プラットフォーム編 〜

どうも、MODEのソフトウェアエンジニアの篠田です。

私の担当エントリではMODEクラウドの各種機能を説明してきました。今日は、MODEクラウドが提供する基本的な管理体系についてお話したいと思います。*1

はじめに

MODE では IoT を利用したデータ活用およびデバイス制御のための管理体系を用意しています。ただデバイスを登録し、整理するという設計だけではうまく IoT のためのサービス基盤としてはうまくいかない、という経験を通してMODEクラウドは設計されています。本エントリではそれが具体的にはどのようになっているかを、かいつまんで説明していきたいと思います。

もし本エントリを読んでご興味を持たれた場合はマニュアルも参照していただけるとより詳しく知ることができますので是非!

概要

MODEクラウドには主に Project, Home, Device, User の4種類の登場人物がでてきます。サービス提供に関わる登場人物です。4つの登場人物は次の図のような階層構造になっています。 Project は複数の Home を持ち、 Device はひとつの Home に所属します。 User は1つ以上の Home に所属することができます。

次の項以降で個別に説明していきましょう。

f:id:takeshinoda-mode:20200615144545p:plain

Project

ひとつのサービスを表し、idを持ちます。例えば、弊社のセンサー・クラウドというサービスはMODE クラウドの一つの Project を持っています。

MODEクラウドの一番大きな単位です。MODEを利用したサービスはまず Project を作るところから始めます。

使い方として、ひとつのサービスをひとつの Project で提供する方法と、ひとつのサービスを複数の Project で提供する方法があります。 マルチテナント的なアプリケーションでは前者、アプリケーションがマルチテナントを想定していない場合は後者の手法が採用されることが多いです。

Project は API 呼び出しのためのキーを持っています。キーとしては権限が広く、Project 内のデータを横断的に参照・書き込みしたり、 Device の制御を横断的にすることができます。

Home

ひとつの空間やグループを表し、id を持ちます。家に見立てて Home という名前がついています。ひとつの Project 内には複数の Home を作ることができ、ひとつの Home はひとつの Project にのみ所属できます。また、Home には後述する複数の「Device」と複数の「ユーザー」を所属させることができます。

Home はデータの格納と可視範囲の単位でもあり、User と Device は所属している Home のみデータを参照したり、書き込んだりすることができます。

特長としては、データは Device に紐付くのでは無く、 Home に紐付くという点です。お察しの通りではあるとは思いますが、デバイスは壊れ得ますので、改善や補修を目的として交換、もしくはひとつのデバイスに付属されていたセンサーが複数のデバイスに分割されるかもしれません。サーバシステムであれば同じサーバとして振る舞うマシンをデータセンタなりクラウド上で交換するだけですが、 IoT では違う id を持つ Device を交換しても同じ種類のデータをクラウドに送信し続けることが想定されます。 Home にデータを紐付ける設計により、サービスは固有のデバイスを意識すること無くデータを取り扱う事ができます。もちろん固有のデバイスとの紐付が大事なデータの場合は、デバイス固有の情報を紐付けられるような情報を付与するなども可能です。

Device

制御するデバイスを指します。IoTっぽくなりましたね。idとキーがあり、 Home 内にデータを書き込んだり、 Home 内のデータを読み込んだりすることができます。

ひとつの Device はひとつの Home に所属することができます。 Home の項でも説明しましたが、モードクラウドへのアクセスの権限は Device 単位で管理されますが、データの管理権限は Home 単位で管理されます。

クラウドとデバイスの通信とデバイスの管理はこの単位で管理されます。 Device からのデータは固有のデバイスを意識しないが、 固有の Device とのコミュニケーションはこの単位。という使い分けになっています。

User

サービスを利用するエンドユーザーを指します。ひとつの User は Device とは違い複数の Home に所属することができます。User は複数の物理的な空間をまたがって利用する可能性があるためです。

User はログインすることでアクセストークンが払い出され、データを参照したり、所属する Home のデバイスを操作する権限が与えられます。

User のMODEクラウドへの登録はメールか電話番号を指定することが出来、MODEクラウド内で管理できます。ただし、一般的にMODEクラウドだけでいわゆるWebアプリケーションを作るユースケースだけではもちろん無いため、連携する外部システムのユーザーを取り込むことができる BYOU という機能も提供されています。詳しくは、How to integrate MODE to your existing web service をご覧ください。

まとめ

このエントリでは駆け足でMODEクラウド内のプラットフォームとしての機能を眺めました。

シンプルな構造ですが、様々なデザインパターンがこの中から選択することができます。特に、データと Device の間に Home があることで、クラウドの抽象度とデバイスの具象性をうまく包み込んでいます。例えば、通常 Home には空間内に複数の Device があような設計を最初に思いつきますが、Device はバラバラに各所にひとつづつ配布するといった用途の場合もあります。この場合、各所ごとにひとつの Home を作成し、それぞれ Device ひとつとすると、デバイスの可換性とデータの永続性を同時に満たすことができます。

MODEではこのような IoT にまつわる設計のパターンを事前に用意しています。サービス提供者はイチから考える必要なく、必要なサービスの価値の提供に集中することができます。

IoT 基盤というと、デバイスとのコミュニケーションに焦点が当てられがちですが、インターネットとモノとの間には巧妙かつシンプルな管理体系も必要になってきます。

このシンプルな構造で様々なパターンのIoTサービスの提供が可能です。是非弊社のソリューションアーキテクトにお問い合わせください。

www.tinkermode.jp

*1:概要としては次のようなエントリがあります IoTクラウドとしてのMODEの紹介 - 概要編 - - MODE JAPAN Blog

MODEにおけるデバイスとのコミュニケーションの方法

MODEでソフトウェアエンジニアをやっている banana-umai です。

今回はMODEプラットフォームにおけるクラウド -> デバイスへのコミュニケーションの方法について紹介したいと思います。MODEではDevice Command(以下Command)とDevice Data Proxy(以下DDP)という2種類の方法によるデバイスへのコミュニケーションの方法が存在します。このドキュメントではそれぞれの方法の特徴をまとめ、それぞれの違いとユースケースを比較したいと思います。

Device Command

Device CommandはMODE CloudからDeviceへのCommandという名前が示す通り状態を持たない一方通行のコミュニケーション方法です。

f:id:banana-umai:20200612124436p:plain

  • AppからMODE CloudにCommandを送信すると、MODE Cloudは即座にそのCommandをDeviceに対して送信します。
  • MODE CloudはAppとDeviceの間の単なるProxyとして働くようなイメージです。Deviceがオフラインの際などにはCommandがDeviceに到達せず、そのまま破棄されます。
  • 詳しい利用方法については以下のオンラインドキュメントを御覧ください。

Device Data Proxy

一方、DDPはデバイス固有のkey-valueストアをDeviceとMODE Cloud間で同期することで実現する状態を持った双方向のコミュニケーション方法です。

f:id:banana-umai:20200612124546p:plain

  • Device Data ProxyはMODE CloudとDeviceの間で共有されるKey-Valueストアのようなものです。
  • AppがMODE Cloud上Key-Valueストア上の任意のデータを更新すると、そのデータはDeviceに対して通知されます。具体的には更新のあったKey-ValueペアがMQTT または WebSocketのコネクションを通じてDeviceに通知されます。
  • AppがDDPに対して更新を行ったタイミングで、Deviceがオフラインだったとしても、書き込まれたデータ自体はMODE Cloud上のキーバリューストアに保存されるので、Deviceがオンラインに復帰した際に最新のKey-ValueストアがDeviceにロードされます。
  • より具体的な利用方法はオンラインドキュメントをご覧ください。

比較

Command

Deviceがネットワークに繋がっていない場合など、CommandがDeviceに届かない場合があります。その場合、Commandは破棄されます。そのため、DDPに比べると機能として劣っているようにも感じられるかもしれませんが、状態を管理しなくても良い分、適切なユースケースにおいては、DDPよりも実装が容易になります。

コマンドに対してのDeviceの動作結果がすぐにわかり、コマンドの再送をオペレータが判断しやすい場合には、Commandを利用することでシンプルに機能を実現できると思われます。

DDP

Device/Cloud間で共有されるKVストアという性質上、Deviceがオフラインからオンラインに復帰した際など、最新のKVの情報がDeviceに送信されます。DDPを用いれば、Commandでできることも実現はできます。ただし、コミュニケーションの形がKey-Valueストアを共有するという形で行われるという性質上、状態の管理や一貫性にある程度気を配る必要が生じます。

DDPの使い所としては、Deviceの設定情報や状態をMODE Cloud経由でWEBアプリケーションやSmart Phoneアプリケーションから管理したい、というような場合や、Deviceに特定の動作をさせたいが、オペレーターから動作結果がわかりづらく、かつ(または)、オンラインになったタイミングで実行されてほしいような場合などがあげられます。


以上、MODEプラットフォームではユースケースによって使い分け可能な2種類のデバイスとのコミュニケーション方法を紹介させて頂きました。