[AWS IoT] OPC-UAのデータをAWS SiteWiseに取り込んでダッシュボードで表示しよう

# はじめに
AWS IoT SiteWiseは産業機器からデータを収集、整理、分析するためのサービスです。何ができるのか、どのように使えばよいのかを確認するために、OPC-UAサーバーからデータを取り込んでダッシュボードで表示する、というデモを作成してみます

# 今回の構成
今回作成するシステムの構成図は以下。AWSのCloud9上にOPC-UAサーバーをたてて、そこからデータをGreengrassのゲートウエイ経由でAWS IoT SiteWiseに取り込み、SiteWiseのダッシュボードでデータを表示します。 ![cloud9.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/5f50078b-0d21-8065-3fd0-fb7556ce8add.jpeg)

# OPC-UAについて
OPC-UAというのは主に工場などでデータをやり取りするために使われる標準規格だそうです。ざっくりいうと、OPC-UAができる前は、工場の機器ごとにそれぞれの通信プロトコルでやり取りをしていて互換性がとれなかったのですが、OPC-UAを使うと標準の手順にのっとって機器の制御ができるということで広く使われているようです。OPC-UAサーバーとOPC-UAクライアントの間で値を読み書きしたり、OPC-UAクライアント側からOPC_UAサーバーの関数を呼び出す、といったことができます。

#OPC-UAの実装
いくつかのOPC-UAの実装がありますが、今回はPythonの実装である、[FreeOpcUa](https://github.com/FreeOpcUa/opcua-asyncio)を使用しました。

#導入手順
## 1）OPC-UAサーバーの起動
まずはAWS上のCloud9の環境を立ち上げます。その環境上でOPC-UAのサーバーを動かしていきます。
```pip install ayncua```
でインストールすることができます。[ここ](https://github.com/FreeOpcUa/opcua-asyncio/blob/master/examples/server-minimal.py)にある、server-minimal.pyの実装を参考にして以下のようなサーバー実装を作成しました。Object上に「1/Temp」「2/Temp」「1/Humid」「2/Humid」という変数を作って1秒毎に乱数の値を入れて更新しています。また、クライアントから参照できる関数をいくつか用意しました。

```python
import logging
import asyncio
import sys
import random
sys.path.insert(0, "..")

from asyncua import ua, Server
from asyncua.common.methods import uamethod



@uamethod
def func_twice(parent, value):
    return value * 2

@uamethod
def multiply(parent, x, y):
    print('multiply method call with parameters: ', x, y)
    return x * y

@uamethod
def square(parent, value):
    return value * value
    

async def main():
    _logger = logging.getLogger('asyncua')
    # setup our server
    server = Server()
    await server.init()
    server.set_endpoint('opc.tcp://0.0.0.0:4840/freeopcua/server/')

    # setup our own namespace, not really necessary but should as spec
    uri = 'http://examples.freeopcua.github.io'
    idx = await server.register_namespace(uri)

    # populating our address space
    # server.nodes, contains links to very common nodes like objects and root
    myobj = await server.nodes.objects.add_object(idx, 'MyObject')
    myvar = await myobj.add_variable(idx, 'MyVariable', 6.7)
    temp1 = await myobj.add_variable(idx, '1/Temp', 25.0)
    temp2 = await myobj.add_variable(idx, '2/Temp', 27.0)
    humid1 = await myobj.add_variable(idx, '1/Humid', 50.0)
    humid2 = await myobj.add_variable(idx, '2/Humid', 55.0)
    myvar2 = await myobj.add_variable(idx, 'MyVariable2', 6.7)
    
    # control value from client
    cont1 = await myobj.add_variable(idx, 'Controller1', 'on')
    cont2 = await myobj.add_variable(idx, 'Controller2', 'on')
    
    # Set to be writable by clients
    await myvar.set_writable()
    await cont1.set_writable()
    await cont2.set_writable()
    
    # function to be called from client
    await myobj.add_method(idx, 'multiply', multiply, [ua.VariantType.Int64, ua.VariantType.Int64], [ua.VariantType.Int64])
    await myobj.add_method(idx, "func_twice", func_twice, [ua.VariantType.Int64], [ua.VariantType.Int64])
    await myobj.add_method(idx, "square", square, [ua.VariantType.Int64], [ua.VariantType.Int64])
    
    _logger.info('Starting server!')
    async with server:
        while True:
            await asyncio.sleep(1)
            new_val = await myvar.get_value() + 0.1
            _logger.info('Set value of %s to %.1f', myvar, new_val)
            
            temp = 25.0 + random.random()*5
            await temp1.write_value(temp)
            _logger.info('Set value of %s to %.1f', temp1, temp)
            
            temp = 27.0 + random.random()*5
            await temp2.write_value(temp)
            _logger.info('Set value of %s to %.1f', temp2, temp)
            
            humid = 50 + random.random()*10
            await humid1.write_value(humid)
            _logger.info('Set value of %s to %.1f', humid1, humid)
            
            humid = 55 + random.random()*10
            await humid2.write_value(humid)
            _logger.info('Set value of %s to %.1f', humid2, humid)
            
            await myvar.write_value(new_val)
            await myvar2.write_value(new_val)
            
            _logger.info('value of %s: %s', cont1, await cont1.read_value())
            _logger.info('value of %s: %s', cont2, await cont2.read_value())
            


if __name__ == '__main__':

    logging.basicConfig(level=logging.DEBUG)
    asyncio.run(main(), debug=True)

```
Cloud9上で以下のように実行します。
``` python3 opcua-server-example1.py```
コンソール上に以下のようなメッセージが表示されます。

```
DEBUG:asyncua.server.uaprocessor:publish request (User(role=<UserRole.User: 3>, name=None))
INFO:asyncua.server.subscription_service:publish request with acks [SubscriptionAcknowledgement(SubscriptionId=79, SequenceNumber=2065)]
INFO:asyncua:Set value of ns=2;i=2 to 213.9
INFO:asyncua:Set value of ns=2;i=3 to 29.6
INFO:asyncua:Set value of ns=2;i=4 to 28.1
INFO:asyncua:Set value of ns=2;i=5 to 53.3
INFO:asyncua:Set value of ns=2;i=6 to 56.5
INFO:asyncua:value of ns=2;i=8: off
INFO:asyncua:value of ns=2;i=9: off
DEBUG:asyncio:poll 998.104 ms took 0.019 ms: 1 events
DEBUG:asyncua.server.binary_server_asyncio:_process_received_message 59 130
```


## 2) Greengrassのゲートウェイの作成
GreengrassのゲートウェイはAWS IoT SiteWiseのマネジメントコンソールからインストーラーを作成することができます。AWS IoT SiteWiseのエッジｰ>ゲートウエイから「ゲートウェイを作成」をクリック。
![スクリーンショット 2022-01-21 11.53.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/457e53a1-83f7-de95-a2d3-005de611fb5b.png)
デフォルトのセットアップで次へ進み、データソースの追加のところで作成したOPC-UAサーバーをデータソースとして追加します。ローカルエンドポイントに、OPC-UAサーバーのエンドポイントを入力し、送信先にAWS IoT SiteWiseを選択します。
![スクリーンショット 2022-01-21 11.57.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/404ffd3f-bd2d-9010-fc9f-c41b926d1226.png)
そうするとインストーラー生成画面が表示されるので、インストーラーを生成し、ファイルをローカルにダウンロードします。ダウンロードしたファイルをCloud9上にアップロードし、以下のように実行します。

``` 
chmod +x sitewise-workshop-Gateway.deploy.sh
sudo ./sitewise-workshop-Gateway.deploy.sh
```
以上でGreengrassゲートウェイが作成されます。Greengrassのマネジメントコンソールでコアデバイスを確認すると、コアデバイスがインストールされており、aws.iot.SiteWiseEdgeCollectorOpcua と aws.iot.SiteWiseEdgePublisher のコンポーネントがインストールされていることがわかります。

## 3) SiteWise側の設定
SiteWise側にデータを取り込むために、a）アセットモデルの作成、b）アセットモデルをもとにしたアセットの作成、c）アセットの測定値にOPC-UAからのデータの紐付け、を行う必要があります。a)のアセットモデルの作成は、工場内での機器の仮想的なデバイスの表現で、アセットの雛形となります。AWS IoT SiteWiseのマネジメントコンソール上から、ビルドｰ>モデルｰ>モデルの作成で作成します。以下のようにOPC-UAから取り込むデータの定義を行います。今回は、温度データ、湿度データ、カウントアップデータを定義しました。
![スクリーンショット 2022-01-21 14.43.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/3dfe2f34-94fc-c71e-ebfd-959552980a33.png)

次に、b）アセットの作成を行います。アセットの作成は、ビルドｰ>アセットｰ>アセットの作成から作成します。対応するモデルの情報とアセットの名前を入力し、アセットの作成をクリック。作成されたアセットの編集ボタンを押し、それぞれの測定項目に対して、以下のようにOPC-UAのObjectとVariableの階層に紐付けます。例えば、温度は「/MyObject/1/Temp」とOPC-UAで定義したので、そのように設定します。MQTT通知ステータスを有効にしておくと、データが入力された場合、```$aws/sitewise/asset-models/[ASSET_MODEL_ID]/assets/[ASSET_ID]/properties/[PROPERTY_ID]```のTopicにデータが通知されます。
![スクリーンショット 2022-01-21 14.50.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/0427b8ed-f8e3-2dd9-4721-996ed8421662.png)

データが来ていることは、マネジメントコンソール上、ビルドｰ>あっセットの測定値の「最新の値」が更新されているかどうかでも確認することができます。

![スクリーンショット 2022-01-21 14.54.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/33d8047c-1703-bd42-e27e-ceb3dfc9989b.png)

##4）ダッシュボードの作成
以上で、OPC-UAサーバーからGreengrassのゲートウェイを経由して、AWS IoT SiteWiseまでデータがきているので、次にダッシュボードを作成して確認します。マネジメントコンソールのモニタリングｰ>ポータルから、「ポータルの作成」をクリックします。ユーザーがポータルにサインインするユーザー認証の方法を設定し、作成をクリックします。ポータルが作成されるので、そこのリンクをブラウザで開き、ログインします。
![スクリーンショット 2022-01-21 15.03.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/d2c9fe1b-bd96-dcd8-9eb3-867f92c30b11.png)
右上にある「Create dashboard」をクリックしDashboardを作成します。作成したアセットの測定値をDrag&Dropで配置してダッシュボードを作成します。

![スクリーンショット 2022-01-21 15.11.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/9a34b325-de3d-00bb-b01c-df777b2ad403.png)

上記のようなダッシュボードを作成することができました。

#まとめ
OPC-UAサーバーからAWS IoT SiteWiseにデータを取り込み、それをダッシュボードで可視化する一連の流れを試してみました。実際にはOPC-UAサーバーの先に様々なフィールドデバイスが動作していて、そのデータをOPC-UAサーバー経由で取得して表示することになりますが、今回は簡易的にOPC-UAサーバーで生成したデータを用いて可視化してみました。Greengrassのゲートウェイの導入などとても簡単にできることがわかりました