Skip to main content
Rapsberry Pi

冬休みの自由研究: EdgeX Foundry (5) ルールエンジンによる自動制御

EdgeX Foundry 関連エントリ

今回のゴール

これまでのエントリでは、仮想デバイスや MQTT デバイスを用いて、デバイスからの情報の収集やデバイスへのコマンドの実行を試してきました。

今回は、ルールエンジンの動作、つまり、

  • 何らかのデバイスのリソースの値が
  • 何らかの条件を満たしたら
  • 別のデバイスでコマンドを実行する

ような自動制御を実際に試します。

前回のエントリ で構成した MQTT デバイスだけを利用してもすぐできるのですが、それだけだとおもしろくないので、新しいデバイスサービスをひとつ追加して、それを組み込みます。具体的には、

  • ルール (1)
    • REST デバイスから送られる値が
    • 80 を越えたら
    • MQTT デバイスにメッセージ HIGH を送信する
  • ルール (2)
    • REST デバイスから送られる値が
    • 20 を下回ったら
    • MQTT デバイスにメッセージ LOW を送信する

ような状態を目指します。図示すると以下のような状態です。

REST デバイスをトリガに MQTT デバイスが自動制御される

今回も GitHub にもろもろ置いてあります ので、こちらをクローンして使います。

$ git clone https://github.com/kurokobo/edgex-lab-handson.git

$ cd lab04

REST デバイスの追加

まだ開発途中で正式リリースには至っていないようですが、device-rest-go と名付けられたデバイスサービスがあります。

REST デバイスサービスの概要

これは、以下のようなデバイスサービスです。

  • REST で POST してくるデバイスに利用する
  • POST されたリクエストボディの中身を値として EdgeX Foundry に取り込む

このデバイスサービスを構成すると、エンドポイントとして

  • /api/v1/resource/<デバイス名>/<リソース名>

が作成され、これに対して POST することで値を取り込んでくれるようになります。デバイス名やリソース名は、前回取り扱った MQTT デバイスと同様、デバイスプロファイルなどの設定ファイルで定義できます。

なお、現時点では情報のやりとりは一方向であり、つまり、デバイス側から POST された値を取り込むのみで、逆にデバイス側へのリクエストの発行はできないようです。 また、JSON を投げても、現時点の実装ではパースはしてくれず単なる文字列として扱われるようです。

今回試す設計

今回は、 device-rest-goREADME.md を参考に、以下のデバイスサービスとデバイスを定義しています。

  • デバイスプロファイル
    • rest/rest.test.device.profile.yml ファイル
    • リソース intfloat を定義
  • デバイスサービス設定
    • rest/configuration.toml ファイル
    • 上記のデバイスプロファイルに紐づけた REST_DEVICE を定義

これにより、

  • /api/v1/resource/REST_DEVICE/int
  • /api/v1/resource/REST_DEVICE/float

に値を POST すれば取り込んでもらえるようになるはずです。

この REST デバイスサービスを含め、今回分の環境は、Docker Compose ファイルに反映済みです

起動

では、今回分の環境をまとめて起動させます。

MQTT デバイスは今回も使うので、 環境に合わせて mqtt/configuration.toml は修正してください。

MQTT ブローカ、MQTT デバイス、EdgeX Foundry の順に起動します。

$ docker run -d --rm --name broker -p 1883:1883 eclipse-mosquitto

$ docker run -d --restart=always --name=mqtt-scripts -v "$(pwd)/mqtt-scripts:/scripts" dersimn/mqtt-scripts --url mqtt://192.168.0.100 --dir /scripts

$ docker-compose up -d

REST デバイスサービスの動作確認

起動が確認できたら、実際にエンドポイントに POST して、データの取り込みを確認します。待ち受けポートは 49986 です。Content-Typetext/plain である必要があります。

$ curl -X POST -H "Content-Type: text/plain" -d 12345 http://localhost:49986/api/v1/resource/REST_DEVICE/int

edgex-device-rest コンテナのログで Pushed event to core data が記録されていれば、値は正常に受け付けられています。

$ docker logs --tail 5 edgex-device-rest
level=DEBUG ts=2020-01-25T07:49:17.0621062Z app=edgex-device-rest source=resthandler.go:82 msg="Received POST for Device=REST_DEVICE Resource=int"
level=DEBUG ts=2020-01-25T07:49:17.0673842Z app=edgex-device-rest source=resthandler.go:101 msg="Content Type is 'text/plain' & Media Type is 'text/plain' and Type is 'Int64'"
level=DEBUG ts=2020-01-25T07:49:17.0726606Z app=edgex-device-rest source=resthandler.go:142 msg="Incoming reading received: Device=REST_DEVICE Resource=int"
level=DEBUG ts=2020-01-25T07:49:17.0766349Z app=edgex-device-rest source=utils.go:75 correlation-id=946f482c-db1f-44b5-8a76-437c4ca3d3e9 msg="SendEvent: EventClient.MarshalEvent encoded event"
level=INFO ts=2020-01-25T07:49:17.0899243Z app=edgex-device-rest source=utils.go:93 Content-Type=application/json correlation-id=946f482c-db1f-44b5-8a76-437c4ca3d3e9 msg="SendEvent: Pushed event to core data"

実際に取り込まれているようです。

$ edgex-cli reading list REST_DEVICE -l 10
Reading ID                              Name    Device          Origin                  Value   Created         Modified        Pushed
b43f7300-1377-4fdb-a82f-44eb497d9568    int     REST_DEVICE     1579938557072646900     12345   3 minutes       3 minutes       50 years

ルールエンジンの利用

お膳立てができたので、本題のルールエンジンを構成していきます。

ルールエンジンの概要

詳細は 公式のドキュメント がわかりやすいですが、端的に言えば、

  • コアサービスまたはエクスポートサービスからデータを受け取る
  • 事前に定義されたルールの条件との合致を確認する
  • 合致していた場合は、そのルールで定義されたアクションを実行する

ような処理をしてくれるサービスです。

実装は現段階では BRMS の Drools ベースとのことなので、Java 製ですね。

ルールエンジンへデータを配信する構成パタンは、ドキュメントでは以下の二つが説明されています。

  • エクスポートサービスのクライアントとして動作させるパタン
    • データは Export Distribution(edgex-export-distro コンテナ)から配信される
    • データの流れが全体で統一されてシンプルに
    • レイテンシやエクスポートサービスのキャパシティなどの面にデメリット
  • コアサービスに直結させるパタン
    • データは Core Data(edgex-core-data コンテナ)から配信される
    • エクスポートサービスをバイパスすることで、レイテンシやパフォーマンス面ではメリットがある
    • データの流れはやや複雑に

この構成パタンの選択や、具体的な接続先の情報は、次で紹介する設定ファイルで指定できます。

ルールエンジンの設定

設定がデフォルトのままでよければ、コンテナイメージの中にすべて含まれているので、敢えて手で投入する必要はありませんが、今回はせっかくなので触れるようにしています。rulesengine フォルダの中のファイル群がそれです。

公式のドキュメント で触れられている設定が含まれるファイルが、rulesengine/application.properties です。

$ cat rulesengine/application.properties
...
export.client=false
...
export.client.registration.url=http://edgex-export-client:48071/api/v1
export.client.registration.name=EdgeXRulesEngine
...
export.zeromq.port=5563
export.zeromq.host=tcp://edgex-core-data
...

export.clientfalse なので、今回はコアサービスから直接データを受け取ります。export.zeromq.host は現時点のデフォルトでは tcp://edgex-app-service-configurable-rules になっており、アプリケーションサービス からデータを受け取る形になっていますが、今回はシンプルにドキュメントに従って tcp://edgex-core-data にしています。

もう一つのファイルが、rulesengine/rule-template.drl です。これはルールそのもののテンプレートとして利用されます。これについては後述します。

ルールの設定

では、実際にルールを設定していきます。現状、GUI でも CLI でもできないので、API を叩きます。標準 GUI には設定画面はあるものの、プルダウンに項目が出てこなくて設定できずでした。

ルールの検討

ルールは以下ような JSON で投入します。

{
    "name": "<ルール名>",
    "condition": {
        "device": "<トリガ条件にするデバイス名>",
        "checks": [
            {
                "parameter": "<トリガ条件にするリソース名>",
                "operand1": "<トリガ条件にする値>",
                "operation": "<比較演算子>",
                "operand2": "<トリガする閾値>"
            }
        ]
    },
    "action": {
        "device": "<操作対象のデバイス ID>",
        "command": "<操作対象のコマンド ID>",
        "body": "<操作対象のコマンドに PUT される内容>"
    },
    "log": "<トリガされたときのログ出力文字列>"
}

全体の構造は、condition で指定した条件を満たしたら action で指定したコマンドがトリガされる、と思えばよいでしょう。

condition での指定値は以下のように組んでいきます。

  • device
    • トリガ条件にするデバイスの名称を指定します。ID では動かないので注意です
    • 今回だと、REST デバイスの値を元にコマンドを実行したいので、REST_DEVICE を指定します
  • parameter
    • トリガ条件にするデバイスのリソース名を指定します。リソース名なので、デバイスプロファイルで指定した名称であり、すなわち Reading の名前でもあります
    • 今回の REST_DEVICE はリソース intfloat を持ちますが、今回は int を指定します
  • operand1
    • 比較元になる値を作ります。デバイスの値は内部では文字列値として扱われているため、数値であれば適切な型にキャストする必要があります
    • Drools が Java ベースのため、ここでは Java の文法で書きます。値に応じて、例えば以下などが使い分けられるでしょう
      • Integer.parseInt(value)
      • Float.parseFloat(value)
  • operation
    • 比較演算子です。<=> などが考えられます
  • operand2
    • 閾値です

action での指定値は、以下のように組みます。

  • device
    • 先ほどと異なり、ここでは ID で指定します
    • 今回は MQ_DEVICE の ID です
    • デバイスの ID は API で確認できます(方法は 過去のエントリ で)
  • command
    • コマンドも ID で指定します
    • 今回は testmessage の ID です
    • コマンドの ID も API で確認できます(方法は 過去のエントリ で)
  • body
    • コマンドの PUT 命令のボディを JSON 形式で指定します
    • めっちゃエスケープが必要です
    • 例えば今回だと、{\\\"message\\\":\\\"HIGH\\\"}{\\\"message\\\":\\\"LOW\\\"} です

最終的に、今回作りたい以下のルール群は、

  • ルール (1)
    • REST デバイスから送られる値が
    • 80 を越えたら
    • MQTT デバイスにメッセージ HIGH を送信する
  • ルール (2)
    • REST デバイスから送られる値が
    • 20 を下回ったら
    • MQTT デバイスにメッセージ LOW を送信する

ひとつめは以下の JSON で、

{
    "name": "rule_int_high",
    "condition": {
        "device": "REST_DEVICE",
        "checks": [
            {
                "parameter": "int",
                "operand1": "Integer.parseInt(value)",
                "operation": ">",
                "operand2": "80"
            }
        ]
    },
    "action": {
        "device": "09dae1fc-e2be-4388-9677-639d2f24c58b",
        "command": "1c7a50e7-5424-4bce-9b9a-29849510580e",
        "body": "{\\\"message\\\":\\\"HIGH\\\"}"
    },
    "log": "Action triggered: The value is too high."
}

ふたつめは以下の JSON であらわされます。

{
    "name": "rule_int_low",
    "condition": {
        "device": "REST_DEVICE",
        "checks": [
            {
                "parameter": "int",
                "operand1": "Integer.parseInt(value)",
                "operation": "<",
                "operand2": "20"
            }
        ]
    },
    "action": {
        "device": "09dae1fc-e2be-4388-9677-639d2f24c58b",
        "command": "1c7a50e7-5424-4bce-9b9a-29849510580e",
        "body": "{\\\"message\\\":\\\"LOW\\\"}"
    },
    "log": "Action triggered: The value is too low."
}

ルールの投入

では、ルールを投入します。エンドポイントは以下です。

  • http://localhost:48075/api/v1/rule

ここに、先の JSON を POST します。

ひとつめのルールの投入。true が返れば成功
ふたつめのルールの投入

ルールの確認

ルールが投入されると、rule-template.drl を元に新しい <ルール名>.drl ファイルが生成されて利用されます。edgex-support-rulesengine 内に配置されるので、ここではコンテナ内のファイルを cat して覗きます。

$ docker exec edgex-support-rulesengine cat /edgex/edgex-support-rulesengine/rules/rule_int_high.drl
package org.edgexfoundry.rules;
global org.edgexfoundry.engine.CommandExecutor executor;
global org.edgexfoundry.support.logging.client.EdgeXLogger logger;
import org.edgexfoundry.domain.core.Event;
import org.edgexfoundry.domain.core.Reading;
import java.util.Map;
rule "rule_int_high"
when
  $e:Event($rlist: readings && device=="REST_DEVICE")
  $r0:Reading(name=="int" && Integer.parseInt(value) > 80) from $rlist
then
executor.fireCommand("09dae1fc-e2be-4388-9677-639d2f24c58b", "1c7a50e7-5424-4bce-9b9a-29849510580e", "{\"message\":\"HIGH\"}");
logger.info("Action triggered: The value is too high.");
end

$ docker exec edgex-support-rulesengine cat /edgex/edgex-support-rulesengine/rules/rule_int_low.drl
package org.edgexfoundry.rules;
global org.edgexfoundry.engine.CommandExecutor executor;
global org.edgexfoundry.support.logging.client.EdgeXLogger logger;
import org.edgexfoundry.domain.core.Event;
import org.edgexfoundry.domain.core.Reading;
import java.util.Map;
rule "rule_int_low"
when
  $e:Event($rlist: readings && device=="REST_DEVICE")
  $r0:Reading(name=="int" && Integer.parseInt(value) < 20) from $rlist
then
executor.fireCommand("09dae1fc-e2be-4388-9677-639d2f24c58b", "1c7a50e7-5424-4bce-9b9a-29849510580e", "{\"message\":\"LOW\"}");
logger.info("Action triggered: The value is too low.");
end

手元の rule-template.drl と見比べると、テンプレートを元に展開されているっぽさがわかりますね。Drools の drl ファイルの文法にあまり詳しくないですが、行われているのが単純な文字列連結なのであれば、operand に指定する値あたりは工夫すると、もうちょっと複雑な計算もできるのかもしれません。試していませんし、インジェクション攻撃っぽいですけど。

なお、現在の API ではルールの中身までは確認できず、ルール名の一覧が取得できるのみのようです。悲しい。

$ curl -s http://localhost:48075/api/v1/rule | jq
[
  "rule_int_high",
  "rule_int_low"
]

ルールエンジンの動作確認

動作を確認していきます。

動きを追いやすくするため、下準備として edgex-support-rulesengine のログを常時表示させて、さらに別のターミナルで MQTT ブローカの全トピックを購読しておくとわかりやすいです。

$ docker logs -f --tail=10 edgex-support-rulesengine
[2020-01-25 13:44:15.023] boot - 6  INFO [main] --- ZeroMQEventSubscriber: JSON event received
[2020-01-25 13:44:15.024] boot - 6  INFO [main] --- ZeroMQEventSubscriber: Event sent to rules engine for device id:  MQ_DEVICE
[2020-01-25 13:44:17.214] boot - 6  INFO [main] --- ZeroMQEventSubscriber: JSON event received
[2020-01-25 13:44:17.216] boot - 6  INFO [main] --- ZeroMQEventSubscriber: Event sent to rules engine for device id:  MQ_DEVICE
...
$ docker run --init --rm -it efrecon/mqtt-client sub -h 192.168.0.100 -t "#" -v
logic/connected 2
DataTopic {"name":"MQ_DEVICE","cmd":"randfloat32","randfloat32":"27.0"}
...

では、REST デバイスの気持ちになって、まずはルールに合致しないデータを REST デバイスサービスに POST します。

$ curl -X POST -H "Content-Type: text/plain" -d 50 http://localhost:49986/api/v1/resource/REST_DEVICE/int

値は取り込まれていますし、

$ edgex-cli reading list REST_DEVICE -l 10
Reading ID                              Name    Device          Origin                  Value   Created         Modified        Pushed
66461804-a894-470f-a099-631ffd4c32cb    int     REST_DEVICE     1579960045357298000     50      About a minute  About a minute  50 years

ルールエンジンにも REST_DEVICE からの値は届いているようなログが出ますが、実際には何もトリガされません。正常です。

$ docker logs -f --tail=10 edgex-support-rulesengine
...
[2020-01-25 13:47:25.370] boot - 6  INFO [main] --- ZeroMQEventSubscriber: JSON event received
[2020-01-25 13:47:25.466] boot - 6  INFO [main] --- ZeroMQEventSubscriber: Event sent to rules engine for device id:  REST_DEVICE
...

つづいて、ルールに合致する値を投げます。

$ curl -X POST -H "Content-Type: text/plain" -d 90 http://192.168.0.100:49986/api/v1/resource/REST_DEVICE/int

ルールエンジンのログでは、指定したログメッセージが記録され、 {"message":"HIGH"} が指定したコマンドにリクエストされたことがわかります。

$ docker logs -f --tail=10 edgex-support-rulesengine
...
[2020-01-25 13:57:26.805] boot - 6  INFO [main] --- ZeroMQEventSubscriber: JSON event received
[2020-01-25 13:57:26.853] boot - 6  INFO [main] --- RuleEngine: Action triggered: The value is too high.
[2020-01-25 13:57:26.853] boot - 6  INFO [SimpleAsyncTaskExecutor-2] --- CommandExecutor: Sending request to:  09dae1fc-e2be-4388-9677-639d2f24c58bfor command:  1c7a50e7-5424-4bce-9b9a-29849510580e with body: {"message":"HIGH"}
[2020-01-25 13:57:26.866] boot - 6  INFO [main] --- RuleEngine: Event triggered 1rules: Event [pushed=0, device=REST_DEVICE, readings=[Reading [pushed=0, name=int, value=90, device=REST_DEVICE]], toString()=BaseObject [id=538aea34-b4ce-4342-88a9-6c08cdac080e, created=0, modified=0, origin=1579960646793054700]]
...

MQTT ブローカ上では、MQTT デバイスに対するコマンド実行が行われた様子がわかります。

$ docker run --init --rm -it efrecon/mqtt-client sub -h 192.168.0.100 -t "#" -v
CommandTopic {"cmd":"message","message":"HIGH","method":"set","uuid":"5e2c4946b8dd790001754b8b"}
ResponseTopic {"cmd":"message","message":"HIGH","method":"set","uuid":"5e2c4946b8dd790001754b8b"}
...

MQTT デバイスの testmessage コマンドに GET すると、値が狙い通りに変更されていることがわかります。

$ curl -s http://localhost:48082/api/v1/device/name/MQ_DEVICE/command/testmessage | jq
{
  "device": "MQ_DEVICE",
  "origin": 1579960983817888000,
  "readings": [
    {
      "origin": 1579960983809258000,
      "device": "MQ_DEVICE",
      "name": "message",
      "value": "HIGH"
    }
  ],
  "EncodedEvent": null
}

同様に、REST デバイスの気持ちになってふたつめのルールに合致する値を投げます。

$ curl -X POST -H "Content-Type: text/plain" -d 10 http://192.168.0.100:49986/api/v1/resource/REST_DEVICE/int

もろもろの処理が動き、MQTT デバイス側の値が変わったことが確認できます。

$ curl -s http://localhost:48082/api/v1/device/name/MQ_DEVICE/command/testmessage | jq
{
  "device": "MQ_DEVICE",
  "origin": 1579961107845061000,
  "readings": [
    {
      "origin": 1579961107837083000,
      "device": "MQ_DEVICE",
      "name": "message",
      "value": "LOW"
    }
  ],
  "EncodedEvent": null
}

まとめ

あるデバイスの値の変化をトリガにして、別のデバイスを制御できることが確認できました。

今回は、REST デバイスを使った関係で、実質手動でトリガさせたに等しい状況でしたが、本来はセンサの値をトリガにアクチュエータを動かすような使い方になるでしょう。REST デバイスの代わりに MQTT デバイスや仮想デバイスのランダム値を condition に指定すれば、似たような状況のテストが可能です。

現段階ではシンプルなルールエンジンしか積まれていませんが、今後エコシステムが成熟してアプリケーションサービスなどが充実してくれば、より複雑な処理も可能になることが期待できます。

EdgeX Foundry 関連エントリ


Rapsberry Pi

冬休みの自由研究: EdgeX Foundry (4) MQTT デバイスサービスの追加

EdgeX Foundry 関連エントリ

おさらい

前々回のエントリ では、下図のとおり、バンドルされている仮想デバイスを利用して動作を確認していました。

前々回のエントリで構成されていたデバイスとデバイスサービス

これらの仮想デバイスは、デバイスからのデータの受け取りやデバイスへのコマンド実行などをテストする目的ではたいへん便利ですが、現実世界とのインタラクションはできません。

そこで今回は、MQTT をインタフェイスにもつデバイス(のシミュレータ)を用意し、下図のように EdgeX Foundry がそのデバイスと実際に(MQTT ブローカを介して)インタラクションできる状態を構成します。

MQTT 対応デバイス(のシミュレータ)を EdgeX Foundry の制御下におく

本エントリの作業内容は、エントリ執筆時点の 公式ドキュメント を基にしています。

今回も、GitHub にもろもろ置いてあります ので、こちらをクローンして使います。

$ git clone https://github.com/kurokobo/edgex-lab-handson.git
 
$ cd lab03

新しいデバイスの仕様と動作確認

まずは、今回新しく EdgeX Foundry の制御下におきたいデバイスの仕様を整理します。その後、実際にそのデバイスのシミュレータを動作させ、仕様通りに動くことを確認します。

新しいデバイスの仕様

今回は、以下のような仕様のデバイスを考えます。

  • 二種類のセンサを持つ
    • randfloat32
    • randfloat64
  • 二種類の文字列情報を持つ
    • ping
    • message
  • トピック DataTopic にセンサ randfloat32 の値を 15 秒ごとに配信する
    • {"name":"MQ_DEVICE","cmd":"randfloat32","randfloat32":"<値>"} の形式で配信する
  • トピック CommandTopic でコマンドを待ち受ける
    • {"name":"MQTT_DEVICE","method":"<get または set>","cmd":"<コマンド名>"} の形式のコマンドを受け取る
  • コマンドを受け取ったら、コマンド応じて処理を実行し、結果をトピック ResponseTopic に配信する
    • コマンドのメソッドが set だった場合は、保存されている message を書き換える
    • コマンドが ping だった場合は、pong を返す
    • コマンドが message だった場合は、保存されている message を返す
    • コマンドが randfloat32 または randfloat64 だった場合は、それぞれ対応するセンサの値を返す
    • 結果は {"name":"MQ_DEVICE","method":"<メソッド>","cmd":"<コマンド名>","<コマンド名>":"<値>"} の形式で配信する

デバイス側が定期的にデータを発信するだけでなく、コマンド操作も受け付けるようなデバイスです。ただしよく見ると、二つあるセンサのうち randfloat64 は、値の定期的な自動配信はありません(これは伏線です)。

シミュレータの起動

今回は、上記のようなデバイスを模したシミュレータを利用します。MQTT を扱うにはブローカが不可欠なので、前回のエントリ と同様にまずはこれを起動します。

$ docker run -d --rm --name broker -p 1883:1883 eclipse-mosquitto
6de5986ebb1b3715179253857952f143fbf19fa77e201980f8573d96558850fd

続けてデバイス(のシミュレータ)の用意です。

Node.js で MQTT を扱えるコンテナ dersimn/mqtt-scripts で、次のようなスクリプトを起動させます。

function getRandomFloat(min, max) {
    return Math.random() * (max - min) + min;
}

const deviceName = "MQ_DEVICE";
let message = "test-message";

// 1. Publish random number every 15 seconds
schedule('*/15 * * * * *', () => {
    let body = {
        "name": deviceName,
        "cmd": "randfloat32",
        "randfloat32": getRandomFloat(25, 29).toFixed(1)
    };
    publish('DataTopic', JSON.stringify(body));
});

// 2. Receive the reading request, then return the response
// 3. Receive the put request, then change the device value
subscribe("CommandTopic", (topic, val) => {
    var data = val;
    if (data.method == "set") {
        message = data[data.cmd]
    } else {
        switch (data.cmd) {
            case "ping":
                data.ping = "pong";
                break;
            case "message":
                data.message = message;
                break;
            case "randfloat32":
                data.randfloat32 = getRandomFloat(25, 29).toFixed(1);
                break;
            case "randfloat64":
                data.randfloat64 = getRandomFloat(10, 1).toFixed(5);
                break;
        }
    }
    publish("ResponseTopic", JSON.stringify(data));
});

これを起動するには、以下のように作業します。IP アドレスは適宜読み替えてください。

$ cd mqtt-scripts
$ docker run -d --restart=always --name=mqtt-scripts -v "$(pwd):/scripts" dersimn/mqtt-scripts --url mqtt://192.168.0.100 --dir /scripts
Unable to find image 'dersimn/mqtt-scripts:latest' locally
...
9d52b4956ebbec60dd0f93bdf9750ff915fefd8e8c58d3ce8bc7ba1429693d2b

シミュレータの動作確認

ブローカの全トピックを購読します。15 秒ごとに DataTopic にセンサ randfloat32 の値が届いていることがわかります。

$ docker run --init --rm -it efrecon/mqtt-client sub -h 192.168.0.100 -t "#" -v
...
DataTopic {"name":"MQ_DEVICE","cmd":"randfloat32","randfloat32":"27.4"}
DataTopic {"name":"MQ_DEVICE","cmd":"randfloat32","randfloat32":"28.6"}

コマンド操作ができることも確認します。別のターミナルで CommandTopic にコマンドを配信します。Windows 環境だと "\" にしないと動かないかもしれません。

$ docker run --init -it --rm efrecon/mqtt-client pub -h 192.168.0.100 -t "CommandTopic" -m '{"name":"MQTT_DEVICE","method":"get","cmd":"ping"}'

$ docker run --init -it --rm efrecon/mqtt-client pub -h 192.168.0.100 -t "CommandTopic" -m '{"name":"MQTT_DEVICE","method":"get","cmd":"randfloat32"}'

$ docker run --init -it --rm efrecon/mqtt-client pub -h 192.168.0.100 -t "CommandTopic" -m '{"name":"MQTT_DEVICE","method":"get","cmd":"randfloat64"}'

$ docker run --init -it --rm efrecon/mqtt-client pub -h 192.168.0.100 -t "CommandTopic" -m '{"name":"MQTT_DEVICE","method":"get","cmd":"message"}'

$ docker run --init -it --rm efrecon/mqtt-client pub -h 192.168.0.100 -t "CommandTopic" -m '{"name":"MQTT_DEVICE","method":"set","cmd":"message","message":"modified-message"}'

$ docker run --init -it --rm efrecon/mqtt-client pub -h 192.168.0.100 -t "CommandTopic" -m '{"name":"MQTT_DEVICE","method":"get","cmd":"message"}'

購読している側では、コマンドが CommandTopic に届き、そのコマンドに応じた応答が ResponseTopic に配信されていることが確認できます。また、message コマンドの結果が、set メソッドの実行前後で変更されていることも確認できます。

$ docker run --init --rm -it efrecon/mqtt-client sub -h 192.168.0.100 -t "#" -v
...
CommandTopic {"name":"MQTT_DEVICE","method":"get","cmd":"ping"}
ResponseTopic {"name":"MQTT_DEVICE","method":"get","cmd":"ping","ping":"pong"}
...
CommandTopic {"name":"MQTT_DEVICE","method":"get","cmd":"randfloat32"}
ResponseTopic {"name":"MQTT_DEVICE","method":"get","cmd":"randfloat32","randfloat32":"27.6"}
...
CommandTopic {"name":"MQTT_DEVICE","method":"get","cmd":"randfloat64"}
ResponseTopic {"name":"MQTT_DEVICE","method":"get","cmd":"randfloat64","randfloat64":"8.39883"}
...
CommandTopic {"name":"MQTT_DEVICE","method":"get","cmd":"message"}
ResponseTopic {"name":"MQTT_DEVICE","method":"get","cmd":"message","message":"test-message"}
...
CommandTopic {"name":"MQTT_DEVICE","method":"set","cmd":"message","message":"modified-message"}
ResponseTopic {"name":"MQTT_DEVICE","method":"set","cmd":"message","message":"modified-message"}
...
CommandTopic {"name":"MQTT_DEVICE","method":"get","cmd":"message"}
ResponseTopic {"name":"MQTT_DEVICE","method":"get","cmd":"message","message":"modified-message"}

ここまでで、デバイス(のシミュレータ)が動作している状態が作れました。ここまでは EdgeX Foundry はまったく関係なく、ただ単に前述の仕様のデバイスを論理的に作っただけです。

デバイスサービスの構成

さて、ここからが本題です。先ほど作ったデバイスを EdgeX Foundry の制御下におくことを考えます。

デバイスサービスの概念

現実世界に存在するデバイスは、それぞれが異なるセンサやスイッチを持っていて、異なるプロトコルをインタフェイスに持つため、それぞれに応じた適切な方法で値の取得や制御命令の発行をする必要があります。

この実態を踏まえ、EdgeX Foundry では、実デバイスと EdgeX Foundry の間を取り持ってくれる存在としてデバイスサービスとよばれるマイクロサービスを定義しています。デバイスサービスは、おおむねデバイスの種類ごとに用意するものと思えばよさそうで、つまり、ひとつのデバイスサービスに対してひとつ以上のデバイスの制御を任せられるようです。

デバイスサービスは、以下のような役割を持ちます。

  • コアサービス層に対して、デバイスからの情報取得やデバイスへの制御命令の発行を行える REST エンドポイントを提供する
  • コアサービス層からの制御命令を実デバイスに合わせた命令に翻訳して実行し、実デバイスを制御する
  • デバイスから送られてきた情報をコアサービス層に合わせた情報に翻訳し、コアサービス層に届ける
  • 事前に定められた場合は、定期的にデバイスに対して特定の処理を行い、結果をコアサービス層に届ける

また、デバイスサービスそのものは、以下のような特徴を持ちます。

  • MQTT や Modbus など業界で多く使われるプロトコルに合わせたデバイスサービスはすでに参考実装が用意されており、複雑でない構成の場合は充分に流用できる
  • 参考実装の流用では不足する場合は、提供されている SDK を利用して気軽に自製できる(C 言語または Go 言語)
  • すべてのデバイスサービスはこの SDK を用いて共通のフレームワークのもとで実装されるため、操作方法や設定方法は自ずと共通化され、相互運用性は高まる
  • EdgeX Foundry を取り巻くエコシステムの成熟により、デバイスのベンダがそのデバイス用のデバイスサービスを提供したり、デバイスにデバイスサービスが組み込まれたり、第三者により開発されたデバイスサービスの充実も期待できる

EdgeX Foundry に限った話ではないですが、プラットフォーム系の製品の場合、製品の発展にはエコシステムの成熟が非常に重要です。この観点では、EdgeX Foundry は、商用環境への利用を謳ったバージョン 1.0 のリリースが 2019 年の冬とついこの前なので、まだまだこれからですね。

デバイスサービスの構成要素

デバイスを制御するデバイスサービスを追加するには、以下の 3 つの要素を考える必要があります。

  • デバイスサービスそのもの
    • コアサービスとデバイスとの間を仲介する、C 言語または Go 言語で実装されたマイクロサービス
    • 今回はコンテナとして動作させる
    • 後述のデバイスプロファイルとデバイスサービス設定を読み込んで動作する
  • デバイスプロファイル(Device Profile)
    • YAML ファイルとして定義
    • 管理対象のデバイス種別が持つリソースや、そのリソースが持つ値の意味、それぞれのリソースに対して行える操作などを定義する
  • デバイスサービス設定(Drvice Service Configuration)
    • configuration.toml ファイルとして定義
    • デバイスサービス自身の設定や、連携する EdgeX Foundry の他のマイクロサービスの情報などを定義する
    • デバイスプロファイルと実際のデバイスを紐づけてデバイスを定義する。デバイスやリソースに対する自動実行処理なども定義する
    • その他、デバイスサービスの動作に必要なパラメータを指定する

デバイスプロファイル

今回は、mqtt.test.device.profile.yml を利用します。中身は以下の通りです。

$ cat mqtt/mqtt.test.device.profile.yml
name: "Test.Device.MQTT.Profile"
manufacturer: "Dell"
model: "MQTT-2"
labels:
- "test"
description: "Test device profile"

deviceResources:
- name: randfloat32
  description: "device random number with Base64 encoding"
  properties:
    value:
      { type: "Float32", size: "4", readWrite: "R", defaultValue: "0.00", minimum: "100.00", maximum: "0.00", floatEncoding: "Base64" }
    units:
      { type: "String", readWrite: "R", defaultValue: "" }
- name: randfloat64
  description: "device random number with e notion"
  properties:
    value:
      { type: "Float64", size: "4", readWrite: "R", defaultValue: "0.00", minimum: "100.00", maximum: "0.00", floatEncoding: "eNotation" }
    units:
      { type: "String", readWrite: "R", defaultValue: "" }
- name: ping
  description: "device awake"
  properties:
    value:
      { type: "String", size: "0", readWrite: "R", defaultValue: "oops" }
    units:
      { type: "String", readWrite: "R", defaultValue: "" }
- name: message
  description: "device notification message"
  properties:
    value:
      { type: "String", size: "0", readWrite: "W" ,scale: "", offset: "", base: ""  }
    units:
      { type: "String", readWrite: "R", defaultValue: "" }

deviceCommands:
- name: testrandfloat32
  get:
    - { index: "1", operation: "get", deviceResource: "randfloat32"}
- name: testrandfloat64
  get:
    - { index: "1", operation: "get", deviceResource: "randfloat64"}
- name: testping
  get:
    - { index: "1", operation: "get", deviceResource: "ping"}
- name: testmessage
  get:
    - { index: "1", operation: "get", deviceResource: "message"}
  set:
    - { index: "1", operation: "set", deviceResource: "message"}

coreCommands:
- name: testrandfloat32
  get:
    path: "/api/v1/device/{deviceId}/testrandfloat32"
    responses:
    - code: "200"
      description: "get the random float32 value"
      expectedValues: ["randfloat32"]
    - code: "500"
      description: "internal server error"
      expectedValues: []
- name: testrandfloat64
  get:
    path: "/api/v1/device/{deviceId}/testrandfloat64"
    responses:
    - code: "200"
      description: "get the random float64 value"
      expectedValues: ["randfloat64"]
    - code: "500"
      description: "internal server error"
      expectedValues: []
- name: testping
  get:
    path: "/api/v1/device/{deviceId}/testping"
    responses:
    - code: "200"
      description: "ping the device"
      expectedValues: ["ping"]
    - code: "500"
      description: "internal server error"
      expectedValues: []
- name: testmessage
  get:
    path: "/api/v1/device/{deviceId}/testmessage"
    responses:
    - code: "200"
      description: "get the message"
      expectedValues: ["message"]
    - code: "500"
      description: "internal server error"
      expectedValues: []
  put:
    path: "/api/v1/device/{deviceId}/testmessage"
    parameterNames: ["message"]
    responses:
    - code: "204"
      description: "set the message."
      expectedValues: []
    - code: "500"
      description: "internal server error"
      expectedValues: []

デバイスプロファイルは、デバイスと一対一ではなく、例えばデバイスのモデルごとにひとつ用意するイメージです 。

この例では、冒頭の 6 行でデバイスのモデルを定義しています。先ほど動作させたデバイス(のシミュレータ)は、Dell 製の MQTT-2 というモデルである、という想定で書かれています。

その後、大きく deviceResourcesdeviceCommandscoreCommands、の三つのセクションが続いています。

deviceResources セクションでは、デバイスが持っているリソースと、それが持つ値の意味を決めています。この例では、このデバイスにリソース randfloat64 があり、それは 0.00 から 100.00 の範囲の Float64 型の値を持つと定義されています。また、message リソースは String 型の値を持ち、他のリソースと違って書き換えが可能(W)と定義されていることがわかります。

deviceCommands セクションでは、このデバイスに対して実行できるコマンドを定義し、そのコマンドのメソッド(get または set)ごとに参照または操作されるリソースを紐付けています。例えば、testrandfloat64 コマンドの get メソッドは、先に deviecResources で定義したリソース randfloat64 からの値の読み取りを行えるように定義されています。また、testmessage コマンドでは、set または get メソッドにより、リソース message の値の取得や変更ができるように定義されています。なお、ここでは実装されていませんが、ひとつのメソッドに複数のリソースを紐付けることも可能です。つまり、一回の get 操作で複数のリソースの値を同時に取得するようにも構成できます。Edge Xpert のドキュメントでは、そのような例が解説され ています。

coreCommands セクションでは、コアサービス層からデバイスに対して実行できるコマンドを定義しています。例えば、コアサービスが testrandfloat64 コマンドに GET リクエストを実行すると、その命令はパス /api/v1/device/{deviceId}/testrandfloat64 に渡されます。このパスは、先の deviceCommands で定義した testrandfloat64 コマンドを表すものです。そしてコアサービスは、結果としてレスポンスコード 200 が返ってきた場合は、そのレスポンスボディに含まれる値を randfloat32 の値として取り込むように構成されています。また、testmessage コマンドのみ、ほかのコマンドと異なり PUT リクエストの動作が定義されています。

デバイスサービス設定

今回は、configuration.toml を利用します。中身は以下の通りです。

このファイルは環境依存の固定値を含みます。自分の環境で試す場合は、IP アドレスを環境に合わせて書き換えてください。

$ cat mqtt/configuration.toml
# configuration.toml
[Writable]
LogLevel = 'DEBUG'

[Service]
Host = "edgex-device-mqtt"
Port = 49982
ConnectRetries = 3
Labels = []
OpenMsg = "device mqtt started"
Timeout = 5000
EnableAsyncReadings = true
AsyncBufferSize = 16

[Registry]
Host = "edgex-core-consul"
Port = 8500
CheckInterval = "10s"
FailLimit = 3
FailWaitTime = 10
Type = "consul"

[Logging]
EnableRemote = false
File = "./device-mqtt.log"

[Clients]
  [Clients.Data]
  Name = "edgex-core-data"
  Protocol = "http"
  Host = "edgex-core-data"
  Port = 48080
  Timeout = 50000

  [Clients.Metadata]
  Name = "edgex-core-metadata"
  Protocol = "http"
  Host = "edgex-core-metadata"
  Port = 48081
  Timeout = 50000

  [Clients.Logging]
  Name = "edgex-support-logging"
  Protocol = "http"
  Host ="edgex-support-logging"
  Port = 48061

[Device]
  DataTransform = true
  InitCmd = ""
  InitCmdArgs = ""
  MaxCmdOps = 128
  MaxCmdValueLen = 256
  RemoveCmd = ""
  RemoveCmdArgs = ""
  ProfilesDir = "/custom-config"

# Pre-define Devices
[[DeviceList]]
  Name = "MQ_DEVICE"
  Profile = "Test.Device.MQTT.Profile"
  Description = "General MQTT device"
  Labels = [ "MQTT"]
  [DeviceList.Protocols]
    [DeviceList.Protocols.mqtt]
       Schema = "tcp"
       Host = "192.168.0.100"
       Port = "1883"
       ClientId = "CommandPublisher"
       User = ""
       Password = ""
       Topic = "CommandTopic"
  [[DeviceList.AutoEvents]]
    Frequency = "30s"
    OnChange = false
    Resource = "testrandfloat64"

# Driver configs
[Driver]
IncomingSchema = "tcp"
IncomingHost = "192.168.0.100"
IncomingPort = "1883"
IncomingUser = ""
IncomingPassword = ""
IncomingQos = "0"
IncomingKeepAlive = "3600"
IncomingClientId = "IncomingDataSubscriber"
IncomingTopic = "DataTopic"
ResponseSchema = "tcp"
ResponseHost = "192.168.0.100"
ResponsePort = "1883"
ResponseUser = ""
ResponsePassword = ""
ResponseQos = "0"
ResponseKeepAlive = "3600"
ResponseClientId = "CommandResponseSubscriber"
ResponseTopic = "ResponseTopic"

このファイルで、実際のデバイスを定義したり、必要なパラメータを指定したりします。

前半部分では EdgeX Foundry 内の他のサービスの情報などを指定しています。

実際のデバイスの定義は [[DeviceList]] の部分です。デバイスプロファイル Test.Device.MQTT.Profile に紐づけてデバイス MQ_DEVICE を定義しています。また、このデバイスに対するコマンドの実行に利用する MQTT ブローカも指定しています。

[[DeviceList.AutoEvents]] の部分では、デバイス側からは自動では配信されない値であった randfloat64 の値を定期的に取得するため、30 秒ごとに testrandfloat64 コマンドを実行するように設定しています。

[Driver] の部分では、デバイスサービスがデバイスから情報を受け取るために購読する MQTT ブローカやトピック名を設定しています。

なお、今回はこのファイルの中で実デバイスの定義もしてしまっていますが、デバイスはあとからも追加が可能です。また、このファイルはあくまで MQTT デバイスサービスの設定ファイルなので、利用したいデバイスサービスが違えば当然この設定ファイルで書くべき内容も変わります。

デバイスサービスを含んだ EdgeX Foundry の起動

設定ファイルができたら、デバイスサービスを含む EdgeX Foundry を起動します。

今回は、Docker Compose ファイルに以下を足しています。これにより、 edgexfoundry/docker-device-mqtt-go イメージがコンテナとして実行され、今回用の設定ファイルが含まれる mqtt ディレクトリがコンテナにマウントさたうえで、起動時にはその中身を利用して EdgeX Foundry にデバイスサービスとデバイスが登録されます。

  device-mqtt:
    image: edgexfoundry/docker-device-mqtt-go:1.1.1
    ports:
      - "49982:49982"
    container_name: edgex-device-mqtt
    hostname: edgex-device-mqtt
    networks:
      - edgex-network
    volumes:
      - db-data:/data/db
      - log-data:/edgex/logs
      - consul-config:/consul/config
      - consul-data:/consul/data
      - ./mqtt:/custom-config
    depends_on:
      - data
      - command
    entrypoint:
      - /device-mqtt
      - --registry=consul://edgex-core-consul:8500
      - --confdir=/custom-config

では、実際にこのファイルを利用して EdgeX Foundry を起動させます。

$ docker-compose up -d

動作確認

まずは、デバイスサービスやデバイスが正常に EdgeX Foundry に認識されているか確認し、その後、値の収集やコマンド操作を確認します。

登録状態の確認

以前のエントリで device-virtual の存在を確認した方法 で、今回はデバイスサービス edgex-device-mqtt とデバイス MQ_DEVICE が確認できれば、登録は成功しています。ラクなのは UI か CLI ですね。

また、これまで紹介しませんでしたが、EdgeX Foundry はマイクロサービス群の状態管理に HashiCorp の Consul を利用しており、各サービスの起動状態や登録状態は、この Consul を通じても確認できます。

Consul の GUI には、http://192.168.0.100:8500/ でアクセスできます。

Consul の GUI

ここで、

  • [Services] タブで edgex-device-mqtt が緑色である
  • [Key/Value] タブで edgex > devices > 1.0edgex-device-mqtt がある
  • [Key/Value] タブで edgex > devices > 1.0 > edgex-device-mqtt > DeviceList > 0 > NameMQ_DEVICE である

あたりが確認できれば、登録はできていると考えてよいでしょう。

デバイスから配信されている値の収集の確認

今回のデバイスは、センサ randfloat32 の値を 15 秒ごとに自動的に MQTT トピックに配信していました。デバイスサービスはこれを受け取れるように構成しましたので、EdgeX Foundry に取り込まれているはずです。

これも、以前のエントリで値を確認した方法 で確認できます。これもラクなのは GUI か CLI ですが、例えば、API で確認する場合は以下の通りです。

$ curl -s http://localhost:48080/api/v1/reading/name/randfloat32/3 | jq
[
  {
    "id": "23fdac76-0870-4b9d-b653-561d8a7c0423",
    "created": 1579707885029,
    "origin": 1579707885005379300,
    "modified": 1579707885029,
    "device": "MQ_DEVICE",
    "name": "randfloat32",
    "value": "QdpmZg=="
  },
  {
    "id": "5c086b1d-6b38-48c0-9aef-9b800f9c72ab",
    "created": 1579707870018,
    "origin": 1579707870004742000,
    "modified": 1579707870018,
    "device": "MQ_DEVICE",
    "name": "randfloat32",
    "value": "QeGZmg=="
  },
  {
    "id": "de67c865-0406-408d-b238-d33de1fe1032",
    "created": 1579707855022,
    "origin": 1579707855011006200,
    "modified": 1579707855022,
    "device": "MQ_DEVICE",
    "name": "randfloat32",
    "value": "Qd2Zmg=="
  }
]

時刻はエポックミリ秒(origin はエポックナノ秒)なので、変換すると 15 秒おきであることが確認できます。

$ date -d "@1579707885.029"
Wed 22 Jan 2020 03:44:45 PM UTC

$ date -d "@1579707870.018"
Wed 22 Jan 2020 03:44:30 PM UTC

$ date -d "@1579707855.022"
Wed 22 Jan 2020 03:44:15 PM UTC

デバイスサービスの自動実行イベントの確認

randfloat64 の値は、自動では配信されなかったため、デバイスサービスの設定(configuration.toml)に [[DeviceList.AutoEvents]] を定義して、デバイスサービス側から 30 秒ごとに自動で収集させていました。

これもデータの蓄積を任意の方法で確認できます。例えば CLI で確認する場合は以下の通りです。

$ edgex-cli reading list MQ_DEVICE -l 10 | grep randfloat64
5df1da1a-91db-41cf-99fa-962e87077395    randfloat64     MQ_DEVICE       1579709342806935600     4.924730e+00    15 seconds      15 seconds      50 years
7c33bb76-c9e9-4ef8-a410-80227e236d13    randfloat64     MQ_DEVICE       1579709312695223100     3.734020e+00    45 seconds      45 seconds      50 years
053987f1-5a89-4cc2-a433-03432caa9039    randfloat64     MQ_DEVICE       1579709282611878800     9.320210e+00    About a minute  About a minute  50 years

これも変換すると 30 秒おきであることが確認できます。

$ date -d "@1579709342.806935600"
Wed Jan 22 16:09:02 UTC 2020

$ date -d "@1579709312.695223100"
Wed Jan 22 16:08:32 UTC 2020

$ date -d "@1579709282.611878800"
Wed Jan 22 16:08:02 UTC 2020

デバイスへのコマンド実行の確認

このデバイスは、リソース message の値は EdgeX Foundry から変更できるような仕様で、デバイスプロファイルでもそれに合わせて PUT リクエストの挙動を定義していました。

では、この操作ができることを確認します。

デバイスプロファイルで定義した各コマンドは、内部では REST エンドポイントでリクエストを待ち受けてており、そしてこの URL は、API 経由で確認できます。具体的には、デバイスを示すエンドポイントへの GET リクエストの結果に含まれます。

$ curl -s http://localhost:48082/api/v1/device/name/MQ_DEVICE | jq
...
  "id": "77fccbb8-f33b-42d8-bf21-6d55cdde2068",
  "name": "MQ_DEVICE",
...
  "commands": [
...
      "name": "testmessage",
...
      "get": {
        "path": "/api/v1/device/{deviceId}/testmessage",
...
        "url": "http://edgex-core-command:48082/api/v1/device/77fccbb8-f33b-42d8-bf21-6d55cdde2068/command/4daa4148-7c0e-4cfd-81cc-b2b740bb4de4"
      },
      "put": {
        "path": "/api/v1/device/{deviceId}/testmessage",
...
        "url": "http://edgex-core-command:48082/api/v1/device/77fccbb8-f33b-42d8-bf21-6d55cdde2068/command/4daa4148-7c0e-4cfd-81cc-b2b740bb4de4",
...

EdgeX Foundry がデバイスにコマンドを実行するときは、内部ではこの URL に対して GET なり PUT なりをリクエストするわけです。そしてデバイスサービスがそれを受け取って、設定したとおりに翻訳してデバイスへの操作が実行され応答が返ることになります。

ここでは実際に、確認できた URL にリクエストを投げます。外部からこの URL を直接叩くときは、ホスト名部分を環境に合わせて書き換えたうえで叩きます。今回であれば、次のような操作で現在値が確認できます。

$ curl -s http://localhost:48082/api/v1/device/77fccbb8-f33b-42d8-bf21-6d55cdde2068/command/4daa4148-7c0e-4cfd-81cc-b2b740bb4de4 | jq
{
  "device": "MQ_DEVICE",
  "origin": 1579710432121478400,
  "readings": [
    {
      "origin": 1579710432109675800,
      "device": "MQ_DEVICE",
      "name": "message",
      "value": "test-message"
    }
  ],
  "EncodedEvent": null
}

まだ何も変更していないので、初期値(mqtt-script.js で定義されている)が返ってきました。

では、変更の PUT を投げます。JSON をリクエストボディに含めて PUT します。

$ curl -s -X PUT -d '{"message":"modified-message"}' http://localhost:48082/api/v1/device/77fccbb8-f33b-42d8-bf21-6d55cdde2068/command/4daa4148-7c0e-4cfd-81cc-b2b740bb4de4

エラーがなければ、再度 GET すると、値が変わっていることが確認できます。

ところで、URL は /api/v1/device/<デバイス ID>/command/<コマンド ID> なパスですが、実はわざわざ ID を調べなくても、/api/v1/device/name/<デバイス名>/command/<コマンド名> でも機能します。というわけで、お好みに合わせてどちらを使ってもよいですが、せっかくなので今度はこちらに GET します。

$ curl -s http://localhost:48082/api/v1/device/name/MQ_DEVICE/command/testmessage | jq
{
  "device": "MQ_DEVICE",
  "origin": 1579710963293832000,
  "readings": [
    {
      "origin": 1579710963283338200,
      "device": "MQ_DEVICE",
      "name": "message",
      "value": "modified-message"
    }
  ],
  "EncodedEvent": null
}

値が変わっていることが確認できました。

なお、このような GET や PUT に相当する操作は、GUI でも実行できます。例えば Closure UI であれば、[DEVICES] 画面のデバイス MQ_DEVICEACTION の [Control Device] アイコンから現在値が確認できます。この裏側では、この画面を開く際に、全部のリソースに対して順番に GET が発行されています。

現在値が確認できる

この画面では、PUT リクエストが可能なリソースには ACTION 欄に Set Value アイコンが出るのですが、実行しようとしても手元の環境では入力欄が表示されず使えませんでした……。

Closure UI ではない標準 UI であれば、PUT も実行できました。

Method を set にして send するだけ

まとめ

新しいデバイス(のシミュレータ)を用意し、それに合わせたデバイスサービスを構成して、このデバイスを EdgeX Foundry の制御下においたうえで、実際の動作を確認できました。

実際のシーンでは、本当のデバイスの本当の仕様に合わせてデバイスサービスを構成する必要があり、場合によってはデバイスサービスそれ自体の実装を含めてカスタマイズが必要になる可能性ももちろんあるわけです。

が、今回確認した通り、デバイスや環境に依存する部分(リソースやコマンドの名前や数、MQTT ブローカのホスト情報やトピック名など)は、デバイスプロファイルやデバイスサービス設定として別ファイルで定義できるようになっていて、デバイスサービスの実装それ自体は、設定ファイルに従って淡々と動くだけでもあります。

例えば Raspberry Pi に接続したセンサの値を取り込みたい場合を考えると、実際のセンサ値を MQTT トピックに投げる部分は自製する必要がありますが、であればその際に、逆に今回使ったこのデバイスサービスの仕様を前提にした投げ方にしてしまうことも可能です。そうすれば、このデバイスサービスをそのまま実際に流用できることになります。

今回は MQTT の例でしたが、それ以外のプロトコル用の参考実装も、環境依存の部分が設定ファイル群で外在化できるような汎用性のある形で作られているようなので、それなりに様々なデバイスで使いまわせそうです。

なお、EdgeX Foundry に取り込まれたデータは、前回のエントリで扱ったエクスポートの設定 や、アプリケーションサービスを構成することで、簡単に持ち出せます。コアサービス層に集約される段階で、すでにデバイスの差異は抽象化されているので、もちろん今回の MQTT で収集したデータも他のデバイスの情報とまったく同じ方法でエクスポート可能です。

EdgeX Foundry 関連エントリ


Rapsberry Pi

冬休みの自由研究: EdgeX Foundry (3) エクスポートサービスによるエクスポート

EdgeX Foundry 関連エントリ

エクスポートの仕組み

前回のエントリ では、EdgeX Foundry を稼働させて、仮想デバイスのデータが蓄積される様子を観察しました。続いては、外部へのエクスポートを試します。

EdgeX Foundry は、エッジ内で閉じて利用することももちろん可能ですが、例えば処理したデータをクラウドに投げたい、オンプレミスの別のシステムに投げたい、など、EdgeX Foundry の外部へデータを連携したいシーンも考えられます。

このようなときに、エクスポートサービスを構成することで、コアサービスに届けられたデータをリアルタイムに外部に送れるようになります。

赤枠が今回関係するところ

EdgeX Foundry では、エクスポート先をエクスポートサービスのクライアントとして定義しています。クライアントは、クライアント登録サービス(図中の CLIENT REGISTRATION、今回の構成では edgex-export-client コンテナ)を通じてデータベースに接続情報を保存することで新規に登録できます。

エクスポート先へのデータの配信は、実際には配信サービス(図中 DISTRIBUTION、edgex-export-distro コンテナ)が行っています。コアサービス層からのイベントの配信を受けて、データベースに登録されたクライアントにデータを送出する役割を担っています。

エクスポートの準備

今回は、エクスポート方法として、

  • MQTT トピックへの配信によるエクスポート
  • REST エンドポイントへの POST によるエクスポート

の二つのパターンを構成します。

前述の通り、エクスポート先は エクスポートサービスのクライアントと考えるため、実際には、

  • MQTT トピックをエクスポートクライアントとして登録する
  • REST エンドポイントをエクスポートクライアントとして登録する

という操作を行います。

さて、今回も GitHub にもろもろ置いてあります ので、こちらをクローンして使います。

$ git clone https://github.com/kurokobo/edgex-lab-handson.git

$ cd lab02

MQTT ブローカの準備

エクスポートクライアントとして MQTT トピックを登録するため、そのトピックを提供する MQTT ブローカを作ります。

本来の用途を考えれば、エッジからさらにフォグかクラウドに送るイメージなので、test.mosquitto.orgCloudMQTT などのクラウドっぽいサービスに投げるほうがそれっぽいですが、今回はお試しなので、ブローカはローカルに立ててしまいます。

といっても、コンテナイメージがあるので、おもむろに起動させるだけです。

$ docker run -d --rm --name broker -p 1883:1883 eclipse-mosquitto
Unable to find image 'eclipse-mosquitto:latest' locally
...
52e107224959223a3132466b5356278f637daa855aadc3a1345c71c193deb4df

起動できたら、簡単にテストします。クライアントもコンテナのイメージが(非公式ですが)あるのでお借りして、適当なトピック名を指定して購読を開始してから、

$ docker run --init --rm -it efrecon/mqtt-client sub -h 192.168.0.100 -t "edgex-handson-topic" -d
Unable to find image 'efrecon/mqtt-client:latest' locally
...
Client mosqsub|6-1f6d3fb35f68 sending CONNECT
Client mosqsub|6-1f6d3fb35f68 received CONNACK (0)
Client mosqsub|6-1f6d3fb35f68 sending SUBSCRIBE (Mid: 1, Topic: edgex-handson-topic, QoS: 0)
Client mosqsub|6-1f6d3fb35f68 received SUBACK
Subscribed (mid: 1): 0

別のコンテナで同じトピックに配信します。

$ docker run --init -it --rm efrecon/mqtt-client pub -h 192.168.0.100 -t "edgex-handson-topic" -d -m "TEST MESSAGE"
Client mosqpub|6-96175cb072bd sending CONNECT
Client mosqpub|6-96175cb072bd received CONNACK (0)
Client mosqpub|6-96175cb072bd sending PUBLISH (d0, q0, r0, m1, 'edgex-handson-topic', ... (20 bytes))
Client mosqpub|6-96175cb072bd sending DISCONNECT

購読している側で、メッセージが配信されてくることが確認できれば成功です。

$ docker run --init --rm -it efrecon/mqtt-client sub -h 192.168.0.100 -t "edgex-handson-topic" -d
...
TEST MESSAGE

デバッグメッセージが出て邪魔な場合は、-d オプションを消すと静かになります。

REST エンドポイントの準備

続いて、エクスポートクライアントとして登録する REST エンドポイントを作ります。

登録すると、REST エンドポイント側には単なる POST リクエストで届くので、POST されたリクエストボディが確認できれば動作確認には充分です。ここでは、Python の Flask で、リクエストボディを標準出力に吐くだけの簡単な Web サーバを作ります。

from flask import Flask, request
from datetime import datetime

app = Flask(__name__)


@app.route("/api/v1/echo", methods=["POST"])
def echo():
    print("--\n{}".format(datetime.now()))
    print(request.get_data().decode("utf-8"))
    return "OK", 200


if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=5000)

リポジトリには配置済みなので、クローンしてある場合はそのまま起動できます。

$ cd lab02/rest-endpoint
$ python ./main.py
 * Serving Flask app "main" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 159-538-312
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

待ち受けができたら、cURL や Postman などで適当な POST リクエストを発行します。

$ curl -X POST -d '{"message": "TEST MESSAGE"}' http://192.168.0.100:5000/api/v1/echo
OK

Web サーバの出力に POST したリクエストボディが表示されれば成功です。

$ python ./main.py
...
--
2020-01-21 00:03:26.503181
{"message": "TEST MESSAGE"}
192.168.0.220 - - [21/Jan/2020 00:03:26] "POST /api/v1/echo HTTP/1.1" 200 -

エクスポートクライアントの登録

ここまでで、エクスポートされればその中身が確認できる状態が整ったので、続けて EdgeX Foundry 側に実際に MQTT トピックや REST エンドポイントをエクスポートクライアントとして登録していきます。

EdgeX Foundry が起動していない場合は、前回のエントリ なども参考にして起動させます。

$ cd lab02
$ docker-compose up -d

エクスポートクライアントの追加は、API や GUI で行えます。CLI では現時点では難しそうです。

API でのエクスポートクライアントの登録

API によるエクスポートクライアントの登録は、edgex-export-client のエンドポイントに JSON で POST することで行えます。cURL や Postman などで実行できます。

MQTT トピックをエクスポートクライアントとして登録するには、以下のような JSON を組んで、

{
    "name": "edgex-handson-mqtt",
    "addressable": {
        "name": "edgex-handson-mqttbroker",
        "protocol": "tcp",
        "address": "192.168.0.100",
        "port": 1883,
        "topic": "edgex-handson-topic"
    },
    "format": "JSON",
    "enable": true,
    "destination": "MQTT_TOPIC"
}

これを http://localhost:48071/api/v1/registration に POST します。 登録されたクライアントの ID が返ってきます。

$ curl -X POST -d '{"name":"edgex-handson-mqtt","addressable":{"name":"edgex-handson-mqttbroker","protocol":"tcp","address":"192.168.0.100","port":1883,"topic":"edgex-handson-topic"},"format":"JSON","enable":true,"destination":"MQTT_TOPIC"}' http://localhost:48071/api/v1/registration
ebdde555-001b-4798-817a-da75b92406c7

登録したらその時点からエクスポートが開始されるので、指定した MQTT トピックを購読すれば、値が届いていることが確認できます。

$ docker run --init --rm -it efrecon/mqtt-client sub -h 192.168.0.100 -t "edgex-handson-topic" -d
...
Client mosqsub|6-1f6d3fb35f68 received PUBLISH (d0, q0, r0, m0, 'edgex-handson-topic', ... (258 bytes))
{"id":"ac6d9f85-7c9f-4300-b6e1-f51f8b8e0e69","device":"Random-Integer-Device","origin":1579534468929275000,"readings":[{"id":"8c9267a0-c8a1-4e9e-9a60-90d7df8966fc","origin":1579534468917108600,"device":"Random-Integer-Device","name":"Int16","value":"8409"}]}
Client mosqsub|6-1f6d3fb35f68 received PUBLISH (d0, q0, r0, m0, 'edgex-handson-topic', ... (263 bytes))
{"id":"0539ed5c-68c3-4a12-a936-743ced169f7d","device":"Random-Integer-Device","origin":1579534468955468400,"readings":[{"id":"4b52dc78-efd9-4234-982c-1a17f21640f3","origin":1579534468943312700,"device":"Random-Integer-Device","name":"Int32","value":"-54648128"}]}

続けて、REST エンドポイントへのエクスポートを追加します。POST する JSON は以下です。

{
    "name": "edgex-handson-rest",
    "addressable": {
        "name": "edgex-handson-restendpoint",
        "protocol": "http",
        "method": "POST",
        "address": "192.168.0.100",
        "port": 5000,
        "path": "/api/v1/echo"
    },
    "format": "JSON",
    "enable": true,
    "destination": "REST_ENDPOINT"
}

POST する先は同様に http://localhost:48071/api/v1/registration です。

$ curl -X POST -d '{"name":"edgex-handson-rest","addressable":{"name":"edgex-handson-restendpoint","protocol":"http","method":"POST","address":"192.168.0.100","port":5000,"path":"/api/v1/echo"},"format":"JSON","enable":true,"destination":"REST_ENDPOINT"}' http://localhost:48071/api/v1/registration
6092b921-b173-4de7-8441-f89461734c17

こちらも追加した段階からエクスポートが開始されるので、REST エンドポイント側で、POST リクエストが来ていることが確認できます。

$ python ./main.py
...
--
2020-01-21 00:47:45.055939
{"id":"a0f8347d-e3d0-4bfc-ae20-98fcb9714224","device":"Random-Integer-Device","origin":1579535264923285900,"readings":[{"id":"9c5313ef-4a04-48f1-bcd4-cea06a08c0f5","origin":1579535264898458900,"device":"Random-Integer-Device","name":"Int16","value":"-18516"}]}
192.168.0.100 - - [21/Jan/2020 00:47:45] "POST /api/v1/echo HTTP/1.1" 200 -
--
2020-01-21 00:47:45.181942
{"id":"7cf0d138-ee3d-47e0-8875-877a5ea1111d","device":"Random-Integer-Device","origin":1579535264973814800,"readings":[{"id":"372eb7bf-9037-43ed-aa53-6d9cd4cafc5a","origin":1579535264962783700,"device":"Random-Integer-Device","name":"Int32","value":"-1557838489"}]}
192.168.0.100 - - [21/Jan/2020 00:47:45] "POST /api/v1/echo HTTP/1.1" 200 -

API でのエクスポートクライアントの確認

登録済みのエクスポート設定は、GET リクエストで確認できます。

$ curl -s http://localhost:48071/api/v1/registration | jq
[
  {
    "id": "ebdde555-001b-4798-817a-da75b92406c7",
    "created": 1579534374854,
    "modified": 1579534374854,
    "origin": 0,
    "name": "edgex-handson-mqtt",
...
  },
  {
    "id": "6092b921-b173-4de7-8441-f89461734c17",
    "created": 1579535149239,
    "modified": 1579535149239,
    "origin": 0,
    "name": "edgex-handson-rest",
...
  }
]

API でのエクスポートクライアントの削除

削除は DELETE リクエストで行えます。エンドポイントはそれぞれ以下です。

  • 削除対象を ID で指定する場合
    • http://localhost:48071/api/v1/registration/id/<ID>
  • 削除対象を名前で指定する場合
    • http://localhost:48071/api/v1/registration/name/<名前>

cURL で実行する場合は以下が例です。

$ curl -X DELETE http://localhost:48071/api/v1/registration/id/ebdde555-001b-4798-817a-da75b92406c7
true

$ curl -X DELETE http://localhost:48071/api/v1/registration/name/edgex-handson-rest
true

GUI でのエクスポートクライアントの登録

ここまで API でがんばってきましたが、前回のエントリ で紹介した Closure UI では、エクスポートクライアントの追加も削除も非常に楽に行えます。

http://localhost:8080/ にアクセスしてログイン後、[EXPORT] を開きます。

まだ何も登録されていない

ここから、画面に従って値を入れていけば完了です。例えば今回の MQTT トピックをクライアントとして登録する場合は、以下のように入力します。

項目
DestinationMQTT Topic
Nameedgex-handson-mqtt
Enableオン(緑)
Export formatJSON
ProtocolTCP
Address192.168.0.100
Port1883
Topicedgex-handson-topic
上記以外デフォルト値

REST エンドポイントの場合は次のようにします。

項目
DestinationREST Endpoint
Nameedgex-handson-rest
Enableオン(緑)
Export formatJSON
ProtocolHTTP
Address192.168.0.100
Port5000
Path/api/v1/echo
上記以外デフォルト値
登録後の状態

GUI でのエクスポートクライアントの削除

削除は、登録後の画面で、削除したいクライアントの右端、[ACTION] 欄にある [Delete Export] アイコンをクリックするだけです。簡単ですね。

削除前に確認が出る

アプリケーションサービスによるエクスポート

……と、意気揚々と書いてきたものの、今回利用したエクスポートサービスは最近のリリースではすでに廃止されており、エクスポートの機能は アプリケーションサービス に統合されているようです。

アプリケーションサービスは、データに対して変換やフィルタなどさまざまな処理を行えますが、その処理のひとつとして、今回行ったようなエクスポートも行えます。複数の処理をパイプライン化してつなげることで、例えば特定のデバイスの値だけフィルタしたあとにエクスポート、などが可能です。

エクスポートサービスでは、すべてのデータがエクスポートサービスを通るため、データ量やエクスポート先が多い場合にパフォーマンス面での問題が考えられますが、アプリケーションサービスでは、各々がコアサービス層のメッセージバスに直接つないでデータを受け取れるようにすることで、こうした問題に対応しているようですね。

SDK も提供されており、自製しなくても単純なフィルタやエクスポートは app-service-configurable を利用して簡単に行えそうです。

アプリケーションサービスを使ったエクスポートは、別のエントリで紹介 していますので、併せてどうぞ。

まとめ

エクスポートサービスを構成して、EdgeX Foundry で収集したデータを外部に送れることが確認できました。エクスポート先の任意のシステムでは、これらの値を取り込んで処理すればよいわけですね。

GUI の画面上やドキュメントでは、GCP や AWS、Azure の IoT サービスもエクスポート先として挙げられていますが、現段階でどこまで動くかは確認できていません。すでに証明書などを用いた認証はできそうなので、どこかで試してみます。

EdgeX Foundry 関連エントリ


Rapsberry Pi

冬休みの自由研究: EdgeX Foundry (2) 導入とデータの確認

EdgeX Foundry 関連エントリ

動かしてみる

EdgeX Foundry、動かしてみましょう。

公式の developer-scripts リポジトリ で、リリースごとに Docker Compose ファイルが提供されているので、動かすだけならとても簡単です。

現時点の最新は Fuji リリースです。ファイルの詳細はリポジトリの README.md に書かれていますが、簡単に試すのであれば、ひとまずはセキュリティ機能を省いた *-no-secty.yml を使うとよいでしょう。

が、今日時点の状態だと、エクスポートサービス層がコメントアウトされていたので、このエントリの内容をそのまま試したい場合は、ぼくのリポジトリに置いてあるヤツ を使ってください。

前提

Git と Docker、Docker Compose が動く環境を用意済みであるものとします。ぼくの家では Ubuntu 上の Docker ですが、もちろん Windows 上の Docker でも大丈夫です。

ファイルの用意と起動

流れを追えるように GitHub にもろもろ置いてあります。これをクローンして、イメージをプルします。

$ git clone https://github.com/kurokobo/edgex-lab-handson.git
$ cd lab01

$ docker-compose pull
Pulling volume            ... done
Pulling consul            ... done
Pulling config-seed       ... done
Pulling mongo             ... done
Pulling logging           ... done
Pulling system            ... done
Pulling notifications     ... done
Pulling metadata          ... done
Pulling data              ... done
Pulling command           ... done
Pulling scheduler         ... done
Pulling app-service-rules ... done
Pulling rulesengine       ... done
Pulling export-client     ... done
Pulling export-distro     ... done
Pulling device-virtual    ... done
Pulling ui                ... done
Pulling portainer         ... done

おもむろに起動させます。

$ docker-compose up -d
Creating network "lab01_edgex-network" with driver "bridge"
Creating network "lab01_default" with the default driver
Creating edgex-files ... done
Creating lab01_portainer_1 ... done
Creating edgex-core-consul ... done
Creating edgex-mongo       ... done
Creating edgex-config-seed ... done
Creating edgex-support-logging ... done
Creating edgex-core-metadata         ... done
Creating edgex-sys-mgmt-agent                 ... done
Creating edgex-support-notifications          ... done
Creating edgex-core-data             ... done
Creating edgex-support-scheduler              ... done
Creating edgex-core-command                   ... done
Creating edgex-app-service-configurable-rules ... done
Creating edgex-export-client                  ... done
Creating edgex-ui-go                          ... done
Creating edgex-device-virtual                 ... done
Creating edgex-export-distro                  ... done
Creating edgex-support-rulesengine            ... done

なお、Windows の場合、edgex-device-virtual がポート 49990(デフォルトのダイナミックポート範囲である 49152 以上)を使っている関係で、運が悪いと以下のようなエラーが出ることがあります。

$ docker-compose up -d
...
Starting edgex-device-virtual ... error

ERROR: for edgex-device-virtual  Cannot start service device-virtual: driver failed programming external connectivity on endpoint edgex-device-virtual (70409888653ac0ce83c1ee9f7df04feb181b35590fc06f0d6fd4e6b511e27cfc): Error starting userland proxy: listen tcp 0.0.0.0:49990: bind: An attempt was made to access a socket in a way forbidden by its access permissions.

ERROR: for device-virtual  Cannot start service device-virtual: driver failed programming external connectivity on endpoint edgex-device-virtual (70409888653ac0ce83c1ee9f7df04feb181b35590fc06f0d6fd4e6b511e27cfc): Error starting userland proxy: listen tcp 0.0.0.0:49990: bind: An attempt was made to access a socket in a way forbidden by its access permissions.
ERROR: Encountered errors while bringing up the project.

この場合、何らかのプロセスが予約したポートと被っている(netsh int ip show excludedportrange protocol=tcp で確認できる)ことが原因なので、いちど OS を再起動するとおそらく治ります。

docker-compose up -d がエラーなく完了したら、docker-compose psedgex-config-seed 以外の StatusUp になっていることを確認します。

$ docker-compose ps
                Name                              Command               State                                                           Ports
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
edgex-app-service-configurable-rules   /app-service-configurable  ...   Up       48095/tcp, 0.0.0.0:48100->48100/tcp
edgex-config-seed                      /edgex/cmd/config-seed/con ...   Exit 0
edgex-core-command                     /core-command --registry - ...   Up       0.0.0.0:48082->48082/tcp
edgex-core-consul                      docker-entrypoint.sh agent ...   Up       8300/tcp, 8301/tcp, 8301/udp, 8302/tcp, 8302/udp, 0.0.0.0:8400->8400/tcp, 0.0.0.0:8500->8500/tcp, 8600/tcp, 8600/udp
edgex-core-data                        /core-data --registry --pr ...   Up       0.0.0.0:48080->48080/tcp, 0.0.0.0:5563->5563/tcp
edgex-core-metadata                    /core-metadata --registry  ...   Up       0.0.0.0:48081->48081/tcp, 48082/tcp
edgex-device-virtual                   /device-virtual --profile= ...   Up       0.0.0.0:49990->49990/tcp
edgex-export-client                    /export-client --registry  ...   Up       0.0.0.0:48071->48071/tcp
edgex-export-distro                    /export-distro --registry  ...   Up       0.0.0.0:48070->48070/tcp, 0.0.0.0:5566->5566/tcp
edgex-files                            /bin/sh -c /usr/bin/tail - ...   Up
edgex-mongo                            /edgex-mongo/bin/edgex-mon ...   Up       0.0.0.0:27017->27017/tcp
edgex-support-logging                  /support-logging --registr ...   Up       0.0.0.0:48061->48061/tcp
edgex-support-notifications            /support-notifications --r ...   Up       0.0.0.0:48060->48060/tcp
edgex-support-rulesengine              /bin/sh -c java -jar -Djav ...   Up       0.0.0.0:48075->48075/tcp
edgex-support-scheduler                /support-scheduler --regis ...   Up       0.0.0.0:48085->48085/tcp
edgex-sys-mgmt-agent                   /sys-mgmt-agent --registry ...   Up       0.0.0.0:48090->48090/tcp
edgex-ui-go                            ./edgex-ui-server                Up       0.0.0.0:4000->4000/tcp
lab01_portainer_1                      /portainer -H unix:///var/ ...   Up       0.0.0.0:9000->9000/tcp

この段階でできているモノ

この段階で、最低限必要な一通りのモノはすでに稼働しています。

この段階でできているモノ

EdgeX Foundry で制御したいデバイスは、この時点では物理的には存在していませんが、テスト用の仮想デバイスが初期状態でいくつか用意されています。これが図中に赤枠で示したもので、

  • デバイス
    • ランダムな値を生成し続けるだけのデバイス
    • ひとつのデバイスが複数のリソースを持つ
      • ここでいうリソースは、デバイスとやり取りできる情報の種類のこと
      • 例えば、今回のデバイスのひとつ Random-Float-Device であれば、Float32Float64 という二種類のリソースを持っていて、別々に制御できる
      • これは具体的には、例えば『空調センサ』という一つのデバイスが『温度』と『湿度』の二つの情報を持っているような状態
    • 外部から、指定したリソースの現在のランダム値を読み取れる
    • 外部から、指定したリソースの数値の生成ロジックを設定できる
  • デバイスサービス
    • 前述のデバイス群(4 つ)を管理するインタフェイス
    • デバイスごとに一定の間隔で所定のリソースの現在の値を取得し、コアサービス層に蓄積する
    • コアサービス層からあるリソースへの GET コマンドを受け取ると、デバイスからそのリソースの現在の値を取得して返す
    • コアサービス層からあるリソースへの POST コマンドを受け取ると、デバイスのそのリソースの数値の生成ロジックを変更する
  • コアサービス
    • デバイスサービスから送られた値を蓄積する
    • (GUI や API を通じて依頼された)デバイスに対する GET や POST のコマンド実行をデバイスサービスに指示する

のように構成されています。

すなわち、最終的に行いたい、デバイスからのセンシングとその値の蓄積、デバイスに対するアクチュエーションが、物理デバイスではないものの、仮想的なデバイスとしてすでに動いていることになります。

実世界でいえば、デバイスは何らかの装置(スイッチとかセンサとかカメラとかいろいろ)であり、外部からはそのデバイスごとにさまざまな手順で情報を取得したり制御したりする必要があります。

デバイスサービスは、そうしたデバイスごとのプロトコルの差異を抽象化したインタフェイスとして振舞い、デバイス種別ごとに単一のコントロールポイントを提供するわけです。

操作手段

操作は GUI、CLI、REST API の三種類で行えます。

といっても、日常的に何らかの操作をすべきものではなく、最初の構築時点でデバイスの登録だったりエクスポートの設定だったりルールエンジンの構成だったりをしてしまったあとは、あとは粛々と運用し続けることになるでしょう。

現段階では、GUI がリッチではありませんが、そういう利用実態を考えればそういうものかもしれません。原則、すべての操作が REST API を介して行えるようになっているので、基本的には Postman などのツールを使って API を叩くものだと思っておいたほうがよさそうです。

GUI での操作

GUI は OSS の範囲では二種類あります。

  • edgex-ui-go
    • 標準の Web GUI
    • 4000 番ポートで待ち受け
  • edgex-ui-closure
    • 多少リッチになった GUI
    • でもところどころ動かない
    • 8080 番ポートで待ち受け
    • 商用の Edge Xpert の GUI はこれがベースの模様

edgex-ui-go が標準の Web GUI です。ブラウザで 4000 番ポートにアクセスして、デフォルトのユーザ admin(パスワードも admin)でログインできます。

ローカルホストで起動させている場合は、http://localhost:4000/ でアクセスできます。

標準 UI のログイン画面

この GUI を通じてもろもろを操作したい場合は、まずはこの GUI で操作する対象を登録する必要があります。[Gateway] の [Add] ボタンで、コンテナをホストしているマシンの IP アドレスを登録します。

登録後の状態

登録したゲートウェイを選択した状態で [Device Service] に遷移すると、先述の device-virtual が登録されている様子が見えるはずです。

device-virtual の登録状況

さらにデバイスサービス device-virtual の [Devices] アイコンを展開すると、このデバイスサービス経由で管理している、先の図で赤枠で示したデバイス群が確認できます。

device-virtual デバイスサービス経由で制御されるデバイス群

さらにデバイスごとの [Commands] を展開すると、デバイスからの情報の取得や、制御命令の実行ができるようになっています。

別の GUI が欲しい場合、edgex-ui-closure というモジュールも用意されています。標準の Docker Compose ファイルには含まれていませんが、今回の Docker Compose ファイルには以下を追記済みなので、起動できているはずです。

  clojure:
    image: edgexfoundry/docker-edgex-ui-clojure:1.1.0
    ports:
      - "8080:8080"
    container_name: edgex-ui-clojure
    hostname: edgex-ui-clojure
    networks:
      - edgex-network
    volumes:
      - db-data:/data/db
      - log-data:/edgex/logs
      - consul-config:/consul/config
      - consul-data:/consul/data
    depends_on:
      - data
      - command

ブラウザで 8080 番ポートにアクセスして、パスワード admin でログインできます。

Clousre UI のログイン画面

この GUI の [DEVICES] 画面でも、先述の device-virtual の配下のデバイスとして制御されている 4 つのデバイスが確認できます。

登録されているデバイス群

また、デバイスごとの右端のアイコン群から、現在値の確認やコマンドの実行が行えます。

CLI での操作

CLI として、現時点ではオフィシャルなリリースではありませんが、edgex-cli が存在しています。

Go 言語で書かれていて、自分で make しないといけないため、環境をつくる必要がありますが、一時的に動かすならコンテナの中で触れます。

以下、コンテナの中で動かす場合の手順です。golang:latest をそのまま使うとページャ(less とか)がなくて部分的にうまく動かなくなるので、途中で less を突っ込んでいます。

$ docker run --rm -it golang
Unable to find image 'golang:latest' locally
latest: Pulling from library/golang
...
Status: Downloaded newer image for golang:latest

# apt update && apt install -y less
Get:1 http://security.debian.org/debian-security buster/updates InRelease [65.4 kB]
...
Processing triggers for mime-support (3.62) ...

# git clone https://github.com/kurokobo/edgex-cli
Cloning into 'edgex-cli'...
...
Resolving deltas: 100% (402/402), done.

# cd edgex-cli

# make install
...
go: finding github.com/BurntSushi/toml v0.3.1

make install がエラーなく完了すれば、コマンドが使える状態です。

ただし、デフォルトだとホスト名 localhost に接続しにいくので、CLI 自体をコンテナ内で動作させている場合は接続できません。

コマンドを一度でも実行すると、接続先の情報が ~/.edgex-cli/config.yaml に保存されるので、コマンドを空打ちしてファイルを作らせたあとに、これを修正します。

# edgex-cli
 _____    _           __  __  _____                     _            
| ____|__| | __ _  ___\ \/ / |  ___|__  _   _ _ __   __| |_ __ _   _ 
|  _| / _` |/ _` |/ _ \\  /  | |_ / _ \| | | | '_ \ / _` | '__| | | |
| |__| (_| | (_| |  __//  \  |  _| (_) | |_| | | | | (_| | |  | |_| |
|_____\__,_|\__, |\___/_/\_\ |_|  \___/ \__,_|_| |_|\__,_|_|   \__, |
            |___/                                              |___/ 
...
Use "edgex [command] --help" for more information about a command.

# ls -la ~
...
drwxr-xr-x 2 root root 4096 Jan 19 04:01 .edgex-cli
...

# grep host ~/.edgex-cli/config.yaml
host: localhost

# sed -i 's/localhost/192.168.0.100/g' ~/.edgex-cli/config.yaml

# grep host ~/.edgex-cli/config.yaml
host: 192.168.0.100

これで準備が完了です。この実装はヘルプが親切なので雰囲気でだいたい触れますが、例えば GUI で確認したようなデバイスサービスやデバイス群は以下のように確認できます。

# edgex-cli deviceservice list
Service ID                              Service Name    Created Operating State 
f2dae296-0313-45e5-a13d-377fadc85276    device-virtual  4 hours ENABLED

# edgex-cli device list
Device ID                               Device Name                     Operating State Device Service  Device Profile  
c085d64c-0da7-4ce3-aa31-53fa2474c38d    Random-Boolean-Device           ENABLED         device-virtual  Random-Boolean-Device
187804e7-62b2-4a9d-95d5-0f723b763048    Random-Integer-Device           ENABLED         device-virtual  Random-Integer-Device
c81a829c-f5d7-4335-987c-0c483d8c7826    Random-UnsignedInteger-Device   ENABLED         device-virtual  Random-UnsignedInteger-Device
cfd1477e-0e84-4993-9e9f-ef89b8f07c92    Random-Float-Device             ENABLED         device-virtual  Random-Float-Device

API での操作

GUI も CLI も結局は REST API にリクエストを投げたレスポンスを整形して表示しているだけなので、これがいちばん生々しく触れる手段です。これでできない操作は基本的にはないはずなので、できなかったらあきらめましょう……。

ただの REST API なので、細かくは 公式のリファレンス を参照してもらえればわかると思います。

例えば、デバイスサービスやデバイスの一覧は以下のエンドポイントに GET を投げると JSON で返ってきます。

  • デバイスサービスの一覧
    • http://localhost:48081/api/v1/deviceservice
  • デバイスサービスの詳細
    • http://localhost:48081/api/v1/deviceservice/name/<デバイスサービス名>
      • デバイスサービス名は一覧で取得できた name の値
    • http://localhost:48081/api/v1/deviceservice/<デバイスサービス ID>
      • デバイスサービス ID は一覧で取得できた id の値
  • デバイスの一覧
    • http://localhost:48081/api/v1/device
  • デバイスの詳細
    • http://localhost:48081/api/v1/device/name/<デバイス名>
      • デバイス名は一覧で取得できた name の値
    • http://localhost:48081/api/v1/device/<デバイス ID>
      • デバイス ID は一覧で取得できた id の値

蓄積データの確認

先述の通り、末端の仮想デバイスで生成されたランダムな値は、デバイスサービス virtual-device を通じてコアサービス層に蓄積されています。実際に動いていることを確認するため、蓄積された値を覗きに行きます。

GUI でのデータの確認

いちばん簡単な手段は、先の Closure UI の [READINGS] 画面です。ここでは以下のように、各デバイスから取得したデータの履歴をサクッと確認できます。また、例えば Random-Float-Device を選択すると、Float32Float64 という二種類のリソースの値が別々に蓄積されていることも読み取れます。

取得した値の確認

蓄積データの構造と API での取得

もっとも詳細な情報が得られる手段は、もちろん API です。

デバイスサービスは、一つ以上の Reading を含む Event オブジェクトを POST することで、コアサービスにデータを送ります。

  • Reading
    • 単一のデバイスの単一のリソースから取得した値の情報
  • Event
    • あるデバイスサービスが一回の処理で単一のデバイスから取得した一つ以上の Reading の集合
    • 例えば『温度と湿度はセットで GET する』ような構成を組んだ場合は、ひとつの Event に二つの Reading が含まれる

Event を取得するエンドポイントに、URL にデバイス名や取得するイベント数を指定して GET すると、Reading を含む情報が得られます。今回の仮想デバイスでは、ひとつの Event にはひとつの Reading が含まれます。

$ curl -s http://localhost:48080/api/v1/event/device/Random-Float-Device/2 | jq
[
  {
    "id": "4949fc7f-069b-49b1-bc52-f905c5109692",
    "device": "Random-Float-Device",
    "created": 1579419158139,
    "modified": 1579419158139,
    "origin": 1579419158137800200,
    "readings": [
      {
        "id": "6dcac272-28ad-4755-972f-ad58b02aa000",
        "created": 1579419158138,
        "origin": 1579419158124917000,
        "modified": 1579419158138,
        "device": "Random-Float-Device",
        "name": "Float32",
        "value": "fvU+Nw=="
      }
    ]
  },
  {
    "id": "f4f62305-ce8c-4f19-960b-ba2977ab0b2b",
    "device": "Random-Float-Device",
    "created": 1579419157643,
    "modified": 1579419157643,
    "origin": 1579419157642273800,
    "readings": [
      {
        "id": "227aa868-6dbe-4e53-af27-c773075eac85",
        "created": 1579419157642,
        "origin": 1579419157630999000,
        "modified": 1579419157642,
        "device": "Random-Float-Device",
        "name": "Float64",
        "value": "1.258607e+308"
      }
    ]
  }
]

特定のリソースの情報に絞りたい場合は、Reading のエンドポイントにリソース名を指定して GET します。

$ curl -s http://localhost:48080/api/v1/reading/name/Float64/2 | jq
[
  {
    "id": "e1e950c0-7533-4664-a606-91c97e8dbb85",
    "created": 1579419397777,
    "origin": 1579419397752820200,
    "modified": 1579419397777,
    "device": "Random-Float-Device",
    "name": "Float64",
    "value": "8.073392e+305"
  },
  {
    "id": "2b30bdf3-cb3e-4bac-bd20-87653e9259f2",
    "created": 1579419367748,
    "origin": 1579419367735956200,
    "modified": 1579419367748,
    "device": "Random-Float-Device",
    "name": "Float64",
    "value": "-3.105079e+307"
  }
]

CLI でのデータの取得

先述の CLI でも、EventReading は取得できます。引数でデバイス名を与える必要があるので、edgex-cli device list でデバイス名を確認してから実行するとよいでしょう。また、-l で数を絞らないと死にます。

# edgex-cli device list                    
Device ID                               Device Name                     Operating State Device Service  Device Profile  

c085d64c-0da7-4ce3-aa31-53fa2474c38d    Random-Boolean-Device           ENABLED         device-virtual  Random-Boolean-Device
187804e7-62b2-4a9d-95d5-0f723b763048    Random-Integer-Device           ENABLED         device-virtual  Random-Integer-Device
c81a829c-f5d7-4335-987c-0c483d8c7826    Random-UnsignedInteger-Device   ENABLED         device-virtual  Random-UnsignedInteger-Device
cfd1477e-0e84-4993-9e9f-ef89b8f07c92    Random-Float-Device             ENABLED         device-virtual  Random-Float-Device

# edgex-cli event list Random-Float-Device -l 5
Event ID                                Device                  Origin                  Created         Modified        
eb8e4094-f090-4326-be23-98babe42485f    Random-Float-Device     1579437080034563100     7 seconds       7 seconds       
a71959a3-99a8-4cba-aecc-e2c4f90f83ce    Random-Float-Device     1579437079284713500     8 seconds       8 seconds        
8d5ca1d7-9e34-4a59-9140-7e07f1fcc00d    Random-Float-Device     1579437050012350100     37 seconds      37 seconds       
cd08a429-1b0d-4639-b62d-0d7e651219d8    Random-Float-Device     1579437049270313900     38 seconds      38 seconds       
04ab9a44-a6bc-409c-b328-6af41b9ab3f7    Random-Float-Device     1579437019994592700     About a minute  About a minute   

# edgex-cli reading list Random-Float-Device -l 5
Reading ID                              Name    Device                  Origin                  Value           Created 
        Modified        Pushed
9153fa3a-12ad-42fa-980f-867e3b5bd750    Float32 Random-Float-Device     1579437080017207500     /oyN8A==        23 seconds      23 seconds      50 years
ce144555-6652-4514-b28e-647e9fcc3dc9    Float64 Random-Float-Device     1579437079271713200     -7.071508e+307  24 seconds      24 seconds      50 years
91f8e6e8-74b7-435c-acf1-f3a40174a7b1    Float32 Random-Float-Device     1579437049996532600     /RPlrg==        53 seconds      53 seconds      50 years
be52e1ff-707a-4826-8013-6c4111db0263    Float64 Random-Float-Device     1579437049255317200     2.928376e+307   54 seconds      54 seconds      50 years
68fa0b12-c248-43ea-9dae-6497f6c343d1    Float32 Random-Float-Device     1579437019973019800     /eOKAw==        About a minute  About a minute  50 years

エンコードされた数値のデコード

API の場合も CLI の場合も、Float32 の値が /oyN8A== のように文字列で表示されていますが、これは内部的にはこの型の数値(を表すバイト列)を Base64 でエンコードして保持しているからです。

数値を保持する際のエンコード有無は、デバイスを追加するときに作成するプロファイルの中で指定できますが、今回は事前構成済みのデバイスを利用しているため、デフォルトの設定に従ってこのようになっています。

エンコードされた数値はデコードすると数値に戻せます。もっといいやり方がありそうな気はしますが、よくわからないので適当なデコーダを作りました。Go が動く環境(CLI での操作で使ったコンテナ内など)で利用できます。

# git clone https://github.com/kurokobo/edgex-decode-base64.git
# cd edgex-decode-base64/

# go run main.go /eOKAw==
8.163255e-37

生のデータベースの探索

ところで、API で生データが取得できると書きましたが、これらのデータは MongoDB に保管されています。MongoDB に直接アクセスすると、実際のデータを確認できます。

外部から MongoDB Compass などでアクセスする場合は、ポート 27017 にユーザ名 core、パスワード password で認証できますが、ここではコンテナのシェルに入って確認します。

edgex-mongo が、MongoDB の実体のコンテナです。これのシェルを取って、MongoDB のプロンプトに入ります。複数のデータベースが存在していることがわかります。

$ docker exec -it edgex-mongo bash

# mongo
MongoDB shell version v4.2.0
...

> show databases
admin                0.000GB
application-service  0.000GB
config               0.000GB
coredata             0.011GB
exportclient         0.000GB
local                0.000GB
logging              0.006GB
metadata             0.000GB
notifications        0.000GB
scheduler            0.000GB

EventReading の値は、coredata 内のコレクションに保持されています。

> use coredata
switched to db coredata

> show collections
event
reading
valueDescriptor

あとは MongoDB のお作法に従って find() などを叩くのみです。遊んだら exit で抜けます。

> db.reading.find().limit(5)
{ "_id" : ObjectId("5e2471bb0e360800014be635"), "created" : NumberLong("1579446715833"), "modified" : NumberLong("1579446715833"), "origin" : NumberLong("1579446715806322700"), "uuid" : "e0af3c99-be57-4cec-a59a-2af2da976d41", "pushed" : NumberLong(0), "device" : "Random-Boolean-Device", "name" : "Bool", "value" : "true" }
{ "_id" : ObjectId("5e2471bb0e360800014be637"), "created" : NumberLong("1579446715857"), "modified" : NumberLong("1579446715857"), "origin" : NumberLong("1579446715831613700"), "uuid" : "c336b11e-8e91-4007-bd3f-1ee63d80e45b", "pushed" : NumberLong(0), "device" : "Random-Boolean-Device", "name" : "Bool", "value" : "false" }
{ "_id" : ObjectId("5e2471c00e360800014be639"), "created" : NumberLong("1579446720829"), "modified" : NumberLong("1579446720829"), "origin" : NumberLong("1579446720815333700"), "uuid" : "2ae4befc-1fee-48ab-b621-b4d56747a446", "pushed" : NumberLong(0), "device" : "Random-Integer-Device", "name" : "Int16", "value" : "9761" }
{ "_id" : ObjectId("5e2471c00e360800014be63b"), "created" : NumberLong("1579446720843"), "modified" : NumberLong("1579446720843"), "origin" : NumberLong("1579446720830168500"), "uuid" : "317ea1f1-0c36-4e6d-a489-b48711ff5dd6", "pushed" : NumberLong(0), "device" : "Random-Integer-Device", "name" : "Int64", "value" : "-9069744988884413669" }
{ "_id" : ObjectId("5e2471c00e360800014be63d"), "created" : NumberLong("1579446720857"), "modified" : NumberLong("1579446720857"), "origin" : NumberLong("1579446720841898700"), "uuid" : "5be3634e-b171-4849-9bac-f13de720c11e", "pushed" : NumberLong(0), "device" : "Random-Integer-Device", "name" : "Int32", "value" : "-913901160" }

> exit
# exit

環境の停止

ひとしきり遊んだら、クリーンアップします。方法がいくつかあります。

  • また起動させたいから、コンテナの停止だけしたいとき
    • docker-compose stop
  • データボリュームは残してコンテナの停止と削除だけしたいとき
    • docker-compose down
  • コンテナの削除だけでなく、ボリュームも Pull したイメージも何もかもを消し去りたいとき
    • docker-compose down --rmi all --volumes

以下は完全消去の例です。

$ docker-compose down --rmi all --volumes
Stopping edgex-device-virtual                 ... done
...
Removing image portainer/portainer

まとめ

EdgeX Foundry を起動させて、仮想のデバイスを利用したデータの取得や蓄積っぷりが、GUI や CLI、API などで確認できました。

次のエントリでは、エクスポート部分を触ります。

EdgeX Foundry 関連エントリ


Rapsberry Pi

冬休みの自由研究: EdgeX Foundry (1) 概要

EdgeX Foundry 関連エントリ

きっかけ

本業が IT やさんではあるもの、IoT とその周辺とは疎遠な日々を過ごしていました。

そんなある日、あるイベントで EdgeX Foundry の存在を教えてもらい、手元に Raspberry Pi もあるものだから、いろいろ調べて実際に触ってみたわけです。IoT 関連のトレンドはまったく追えている気がしていなかったので、良い機会だしお勉強しましょうということで。

その結果、最新の Fuji リリースがすでに家で動いているのですが、技術的な諸々は長くなったので別のエントリにします。今回は概要編的なヤツです。

EdgeX Foundry とは

公式サイト によれば、

The World’s First Plug and Play Ecosystem-Enabled Open Platform for the IoT Edge.

A highly flexible and scalable open software framework that facilitates interoperability between devices and applications at the IoT Edge, along with a consistent foundation for security and manageability regardless of use case.  

https://www.edgexfoundry.org/

とされています。

ひとまずは、

  • オープンソースで
  • IoT のエッジコンピューティングを実現する
  • フレームワークである

と思って向き合えば、大外れではないでしょう。

これは Linux Foundation が立ち上げた LF Edge のプロジェクトのひとつで、LF Edge には 現時点で 80 社以上 がメンバとして掲載されています。

つまり、素性のわからない謎の OSS ということではまったくなく、 実際のコントリビュータとしても Dell Technologies や Intel などの面々が精力的に活動しています。後ろ盾があるのは安心感がありますね。

家のお手軽空調センサ群。
Raspberry Pi 経由で EdgeX Foundry に値を集約している。

EdgeX Foundry エッジコンピューティング

クラウド、というキーワードが盛り上がり始めたころは、『とにかく全部クラウドに持っていけ! 何でもクラウドに投げこめ!』みたいな風潮も少なからずあったと思います。

が、その後、ハイブリッドクラウドやマルチクラウドなど諸々の議論を経て、最近は、いやいや、何をするにもやっぱり使い分けだよね、という共通認識が形成されつつあり、どちらかといえばふたたび分散型に(前向きに)回帰しています。

IoT の文脈では、処理すべきデータが生まれる場所、あるいは操作すべきデバイスが存在する場所は、少なくともクラウドの中ではないわけで、つまり工場やら家やらオフィスやら屋外やら、目の前にある現実の身近などこかであるわけです。

であれば、

  • 何万もあるセンサが毎秒クラウドに生データを送り続けるのも非効率だ
  • クラウド上で分析にかけるにしても、大量の生データである必要はなく、分析に適した形式にフィルタ(または変換)されたデータだけあればよい
  • クラウドに送って、クラウドで処理して、クラウドから命令が来るとなると、端的に遅延がありすぎてリアルタイムに制御できない
  • 機微データなのでそのままはクラウドに送りたくない

など、コストやら応答速度やらセキュリティやらの諸々で、

  • だったらエッジ側に閉じて処理してしまったほうがよくない?
  • 最終的にクラウドに集めるにしても、必要なデータを必要な形にエッジ側で加工してから送ればよくない?

ということになり、ここからいわゆる『エッジコンピューティング』の概念が導出されてきます。5G も最近話題ですが、これもエッジコンピューティングの文脈でもよく出てきますね。

Rapsberry Pi
Raspberry Pi。
センサの値を MQTT トピックに定期的に投げ込んでいる。

とはいえ、こうしたエッジコンピューティングを実現しようとしても、いろいろなデバイスがいろいろなプロトコルを使っていることもあり、 特に産業分野での IoT の文脈では、いわゆる鉄板のアーキテクチャ的なモノが成熟しておらず、デバイスと上位の分析基盤やアプリケーションとの円滑な連携にはまだまだ課題もあるようです。

EdgeX Foundry は、こうした IoT の世界におけるエッジコンピューティングのためのプラットフォームとしてフレームワークを提供するものであり、具体的には、デバイス群の制御やそれらからのデータの収集、加工、分析、外部への送出など、必要な一連の機能それ自体とそのデザイン手段を一元的に提供するものといえる、のではないでしょうか。

このフレームワークを介することで、データへのアクセスやコントロールの手段が抽象化されるため、デバイスと分析基盤やアプリケーションの相互の運用性が高まる、のだと思います。

EdgeX Foundry のアーキテクチャ

で、実装面をもう少し調べていくわけですが、公式のドキュメント がけっこう充実しているので、これを紐解くのがよさそうです。

見ていくと、EdgeX Foundry が多数のマイクロサービスの集合体として実装されていることがわかります。 以下、アーキテクチャの画像を引用します。

EdgeX Foundry Architecture
EdgeX Foundry Architecture
https://docs.edgexfoundry.org/2.0/general/EdgeX_architecture.png

データフローにかかわる部分だと、全体で大きく 4 つのレイヤで構成されています。レイヤごとに役割をざっくり押さえておくとよさそうです。

触ってみた感触も踏まえて、大まかにぼくなりの書き方をすると、実デバイスに近い(サウスサイド)側から、

  • デバイスサービス層
    • 実デバイスからデータを受け取って、コアサービス層に送る
    • コアサービス層から命令を受け取って、実デバイスを操作する
  • コアサービス層
    • デバイスサービス層からのデータを蓄積する
    • サポートサービス層(のルールエンジン)から命令を受け取って、デバイスサービス層にコマンドを発行する
    • エクスポートサービス層にデータを送る
    • EdgeX Foundry 内のサービス群やオブジェクトのメタデータを管理する
  • サポートサービス層
    • いわゆる分析機能(今のところは簡単なルールエンジンのみ)を提供する
    • アラートやロギングの機能を提供する
    • エクスポート済みのデータの削除を行う
  • エクスポートサービス層
    • EdgeX Foundry の外にデータを送出する

というところでしょうか。

実装は Go 言語が主のようです。動作させるためのハードウェア要件も厳しくなく、Raspberry Pi 上でも動かせるみたいですね。また、起動させるだけなら Docker Compose でさくっとできます。

家では、vSphere 上の Ubuntu 仮想マシン上に Docker Compose で動作させています。

EdgeX Foundry でできそうなこと

さて、で、つまり何ができるの、というところですが、ざっくり技術的な目線では、

  • 様々なデバイスからの情報取得やそれらの操作が EdgeX Foundry に集約できる
    • デバイスサービスが実デバイスに対するインタフェイスとして機能する
    • デバイスごとの機種やプロトコルの差異をデバイスサービスが吸収するので、実デバイスの実装を意識しないで統一された手法(REST API や GUI)でデバイスにアクセスできる
    • デバイスサービスはさまざまなプロトコル(REST、Modbus、MQTT、ZigBee、……)に対応しており、参考実装が GitHub にある
    • SDK が提供され、比較的容易に自製できる
    • デバイスサービスは製品への組み込みも視野に入っている
  • ルールエンジンにより、イベントドリブンなデバイスの制御ロジックを EdgeX Foundry で完結する形で組み込める
    • あるデバイスのセンサの値が閾値を越えたら別のデバイスのスイッチを入れる、など
    • EdgeX Foundry 内の通信も REST API と ZeroMQ なので、他の分析基盤やアプリケーションとも連携させられそう
  • 集約したデータは外部へ送り出せる
    • MQTT での発行や REST エンドポイントへの POST などができる
    • いわゆるストア&フォワード、常時接続でない環境でいったん自分の中に貯めておいて接続できたらまとめて送る、ができる模様
  • マイクロサービス化されており、フォグコンピューティング的な階層化やサービス単位での機能追加、入れ替えにも柔軟に対応できる
    • どこに何をおいてもよい
    • エッジに全部おいてもいいし、クラウドに全部おいてもいい
    • 例えば、デバイスサービスは Raspberry Pi、コアサービスは普通のサーバに置いて、分析系はつよいマシンに置くとか
    • エコシステムが成熟するとアドオンできるサービスも増えそうだ

などは挙げられそうです。

関連しそうなプロジェクト

EdgeX Foundry は OSS ですが、これを商用製品としたのが IOTech の Edge Xpert だそうです。

このドキュメントは追い切れていませんが、少なくともデバイスサービスとエクスポートサービスのバリエーションは、OSS 版に加えてバンドルされたテンプレートがたくさんありそうでした。便利そうです。

また、IOTech は組み込み系などリソースや時間の制約に厳しい環境のための軽量・高速版ともいえる Edge XRT も発表しています。

家の加湿器。
EdgeX Foundry から AWS などいろいろ経由して制御される。

ほか、競合する製品というと、そもそも知っている範囲が狭いのですが、オープンソース系だと KubeEdge、パブリッククラウド系だと Google Cloud IoT や Azure IoT Edge、AWS IoT Greengrass あたりになるでしょうか。

とはいえ、パブリッククラウド系の IoT サービスだと、どうしても中心はクラウド側で、最終的にはクラウドありきになってくるので、言い方が『クラウドで行う処理をエッジにオフロードする』というニュアンスになってきますね。トップダウン的な。

EdgeX Foundry は、その文脈でいうとボトムアップ的なアプローチっぽさがあります。この見方では、クラウドは必ずしも登場はしません。

動かしてみる

冒頭で書いたとおり、最新リリースの Fuji がいま家で動いています。

せっかくなので、導入部分、デバイスから値を貯める部分、デバイスを動かす部分、エクスポートする部分、ルールエンジンを使う部分、あたりに分けて、今後エントリを書いてみようかという気持ちでいます。先走ってタイトルに (1) って付けてしまった……。

いろいろリンク

EdgeX Foundry 関連エントリ