Skip to main content
Rapsberry Pi

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

EdgeX Foundry 関連エントリ

おさらい

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

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

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

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

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

本エントリの作業内容は、エントリ執筆時点の公式ドキュメント(6.5. MQTT – Adding a Device to EdgeX)を基にしています。

今回も、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 &amp;&amp; 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/_images/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 関連エントリ


ゲームボーイアドバンスの液晶を交換した話

高校二年生のとき、中学生の頃に買ったゲームボーイアドバンスを、初めていろいろ改造した。

ホワイトだった本体をウレタン塗料でがっつり黒に塗り、電源の LED を緑から青に変え、そして Afterburner と呼ばれるフロントライトを追加して、さらにその光量調節をボタン操作で行えるようにするチップ(Stealth Dimmer Chip)を取り付け。

今では考えられないけれど、ゲームボーイアドバンス(というか当時の携帯ゲーム機全般)はもともと光源を何も積んでおらず、致命的に画面が暗かった。

だからこのフロントライトも、当時はけっこうインパクトがあり、わくわくしながら取り付けたものである。実際、暗所でもこれだけの視認性が得られる。

で、それから 15 年ちかく経った最近、ゲームボーイアドバンス用の交換用のバックライト付き液晶の存在を知った。もちろん純正品ではない。

とくに遊びたいゲームがあったわけではないのだけれど、久しぶりに何かを改造したい欲がもこもこしてきたので、とりあえずポチり、台風で家に引きこもっている最中にちまちまと交換したという具合。

まずは前述の Stealth Dimmer Chip を取り外さないといけない。この半田付け、高校生のぼくにはものすごく難易度が高くて苦労した記憶がある。今見ると配線はキレイだけれど、半田付けがものすごく汚い……。イモもいいところである。とはいえ、15 年動いてくれたわけだけれど。

これがもともと付けていた Afterburner。雑に言えば、液晶の手前に配置する、光る透明な板。

こんな感じで光る。

懐かしさを覚えつつ、全部とっぱらって、左のモノに交換。

試しに通電させた時点で、技術の進歩ってすごいなあと思った。明るいし…… ドットが細かいし……。

この液晶、解像度が元の 4 倍で、従来の 1 ドットを 2 × 2 の 4 ドットで描画している。ドットピッチが倍なので、単に明るいだけでなく、明らかに見た目がきれい。

ただし、元の液晶よりも一回り大きいので、本体のケースをごりごり物理的に削らないと所定の位置に収まらない。

単に部品を交換するだけだとあっさりすぎるので、改造している感があってちょうどよかった(?)。

で、こうなりました。

とても明るい。そしてきれい。暗所でこれだけ見えて恐ろしい。コントラストがエグい。

並べると圧倒的な差。満足です。