docker+opcua+griddbで簡単なシステムを考えてみる

## はじめに
docker環境で設備から取得した値をopcuaに集約し、定周期でGridDBに蓄積する超簡単なシステムを考えてみます。
(設備はないので適当な値をopcタグに書き込みます)
イメージは↓の感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168779/b945c7ac-1a6f-0d64-b654-1ceb2f267b8d.png)
[GridDB プログラミングチュートリアル](https://www.toshiba-sol.co.jp/pro/griddb/docs-jp/v4/GridDB_ProgrammingTutorial.html)を自分なりにアレンジ加えた感じです。
家でお勉強用にちゃちゃっとやったやつなので全体的に適当です。
またもや自分の備忘録ですね。

## sample
[mototoke/opc-ua-griddb-docker](https://github.com/mototoke/opc-ua-griddb-docker)にソース一式まとめてます。

## docker-compose.yml

環境変数とかdockerネットワークとか書いてたらけっこうデカめのymlになりました。
<details><summary>docker-compose.yml</summary><div>

```docker-compose.yml
version: '3'
services:
    # データアップローダー用のNoSql(GridDB)
    # Dockerfile参考URL: https://github.com/griddb/griddb-docker
    griddb:
        container_name: griddb-docker
        build:
            context: ./griddb
            dockerfile: Dockerfile
        ports:
            - 10011:10001
        volumes:
            - "vol:/var/lib/gridstore"
        networks:
            my_network:
                ipv4_address: 192.168.145.10

    # データアップローダー、管理アプリケーションコンテナ(Java - opc, griddb)
    # Dockerfile参考URL: https://github.com/griddb/griddb-docker/tree/main/jdbc
    data-register:
        container_name: data-register-app
        build:
            context: ./data-register
            dockerfile: Dockerfile
        tty: true
        environment:
            # OpcUa
            ACCESS_IP: "192.168.145.25"
            ACCESS_PORT: "4840"
            DEVICE_1_NODE_ID: "1"
            DEVICE_1_QUARLIFIED_NAME: "2001"
            DEVICE_2_NODE_ID: "2"
            DEVICE_2_QUARLIFIED_NAME: "2"
            DEVICE_3_NODE_ID: "3"
            DEVICE_3_QUARLIFIED_NAME: "1"
            EXECUTINON_CYCLE: "3000"
            # GridDB
            NOTIFICATION_ADDRESS: "239.0.0.1"
            NOTIFICATION_PORT: "31999"
            CULSTER_NAME: "dockerGridDB"
            USER: "admin"
            PASSWORD: "admin"
        volumes:
            - "./data-register/project:/root/project"
            - "vol:/var/lib/gridstore"
        depends_on:
            - griddb
        networks:
            my_network:
                ipv4_address: 192.168.145.20

    opc-server:
        container_name: opc-server
        build:
            context: ./opc-server
            dockerfile: Dockerfile
        tty: true
        environment:
            ACCESS_URL: "opc.tcp://0.0.0.0:4840/opc-ua/griddb/server"
        volumes:
            - "./opc-server/project:/root/project"
            - "vol:/var/lib/gridstore"
        depends_on:
            - griddb
        ports:
            - 4840:4840
        networks:
            my_network:
                ipv4_address: 192.168.145.25

    opc-client-1:
        container_name: opc-client-1
        build:
            context: ./opc-client
            dockerfile: Dockerfile
        tty: true
        environment:
            ACCESS_IP: "192.168.145.25"
            ACCESS_PORT: "4840"
            NODE_ID: "1"
            QUARLIFIED_NAME: "2001"
            EXECUTINON_CYCLE: "3000"
            METHOD_TYPE: "Random"
        volumes:
            - "./opc-client/project:/root/project"
            - "vol:/var/lib/gridstore"
        depends_on:
            - griddb
        networks:
            my_network:
                ipv4_address: 192.168.145.31

    opc-client-2:
        container_name: opc-client-2
        build:
            context: ./opc-client
            dockerfile: Dockerfile
        tty: true
        environment:
            ACCESS_IP: "192.168.145.25"
            ACCESS_PORT: "4840"
            NODE_ID: "2"
            QUARLIFIED_NAME: "2"
            EXECUTINON_CYCLE: "1000"
            METHOD_TYPE: "Fluctuation"
        volumes:
            - "./opc-client/project:/root/project"
            - "vol:/var/lib/gridstore"
        depends_on:
            - griddb
        networks:
            my_network:
                ipv4_address: 192.168.145.32

    opc-client-3:
        container_name: opc-client-3
        build:
            context: ./opc-client
            dockerfile: Dockerfile
        tty: true
        environment:
            ACCESS_IP: "192.168.145.25"
            ACCESS_PORT: "4840"
            NODE_ID: "3"
            QUARLIFIED_NAME: "1"
            EXECUTINON_CYCLE: "1000"
            METHOD_TYPE: "Fluctuation"
        volumes:
            - "./opc-client/project:/root/project"
            - "vol:/var/lib/gridstore"
        depends_on:
            - griddb
        networks:
            my_network:
                ipv4_address: 192.168.145.33
    
    # 参考URL: https://github.com/griddb/griddb-datasource
    grafana_plugin:
        container_name: grafana
        build:
            context: ./grafana-plugin
            dockerfile: Dockerfile
        ports:
            - 3000:3000

volumes:
    vol:

networks:
    my_network:
        ipam:
            driver: default
            config:
                - subnet: 192.168.145.0/24
```
</div></details>

### griddb
ほとんど参考URLの通りです。
Dockerfileはgriddb-cliのインストールを追加しています。

```yaml:docker-compose.yml
# データアップローダー用のNoSql(GridDB)
    # Dockerfile参考URL: https://github.com/griddb/griddb-docker
    griddb:
        container_name: griddb-docker
        build:
            context: ./griddb
            dockerfile: Dockerfile
        ports:
            - 10011:10001
        volumes:
            - "vol:/var/lib/gridstore"
        networks:
            my_network:
                ipv4_address: 192.168.145.10
```

```dockerfile:
FROM ubuntu:18.04

# You can download griddb V4.5.2 directly at https://github.com/griddb/griddb/releases/tag/v4.5.2
ENV GRIDDB_VERSION=4.5.2
ENV GRIDDB_DOWNLOAD_SHA512=92d0e382c8d694c2b37274fa37785e2bdb9d6ad8aee0f559e75a528ad171aea1221091ca24db5e2f90442fd92042a6307198d74d0eb1b5f9a23659a26ca7b609
ENV GS_HOME=/var/lib/gridstore
ENV GS_LOG=/var/lib/gridstore/log
ENV PORTS=10001

# Install griddb server
RUN set -eux \
    && apt-get update \
# Install dependency for griddb
    && apt-get install -y dpkg python wget \
    && apt-get clean all \
# Download package griddb server
    && wget -q https://github.com/griddb/griddb/releases/download/v${GRIDDB_VERSION}/griddb_${GRIDDB_VERSION}_amd64.deb \
# Check sha512sum package
    && echo "$GRIDDB_DOWNLOAD_SHA512 griddb_${GRIDDB_VERSION}_amd64.deb" | sha512sum --strict --check \
# Install package griddb server
    && dpkg -i griddb_${GRIDDB_VERSION}_amd64.deb \
# Remove package
    && rm griddb_${GRIDDB_VERSION}_amd64.deb

ENV GRIDDB_BASE_VERSION=4.6.0
ENV GRIDDB_MINOR_VERSION=4.6.0-1

# Install griddb cli
RUN set -eux \
    && apt-get update \
    && apt-get install -y default-jre \
    && apt-get install -y openjdk-8-jdk \
    # Download package griddb server
    && wget -q https://github.com/griddb/cli/releases/download/v${GRIDDB_BASE_VERSION}/griddb-cli_${GRIDDB_BASE_VERSION}_amd64.deb \
    # Install package griddb cli
    && dpkg -i griddb-cli_${GRIDDB_BASE_VERSION}_amd64.deb \
    # Remove package
    && rm griddb-cli_${GRIDDB_BASE_VERSION}_amd64.deb


VOLUME /var/lib/gridstore

# Config file for griddb
COPY start-griddb.sh /
USER gsadm
ENTRYPOINT ["/bin/bash", "/start-griddb.sh"]
EXPOSE $PORTS
CMD ["griddb"]
```

## opc-server
docker-compose.yamlとdockerfileはそんなに大したことしてないです。
composeのenvironmentでopcのURLを指定して、client用のタグを用意しているだけです。
[FreeOpcUa/opcua-asyncio](https://github.com/FreeOpcUa/opcua-asyncio)の[server-minimal.py](https://github.com/FreeOpcUa/opcua-asyncio/blob/master/examples/server-minimal.py)をちょっとだけ改造してます。
すごくシンプル！

```python:main.py
import logging
import asyncio
import os
import sys
sys.path.insert(0, "..")

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

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

async def main():
    _logger = logging.getLogger('asyncua')
    # setup our server
    server = Server()
    await server.init()

    # 環境変数から設定値を取得
    url = os.environ['ACCESS_URL']
    print(url)
    server.set_endpoint(url)

    server.set_security_policy([
                ua.SecurityPolicyType.NoSecurity,
                ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt,
                ua.SecurityPolicyType.Basic256Sha256_Sign])

    # 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, 'ScalarTypes')
    # Set MyVariable to be writable by clients
    dev1 = await myobj.add_variable(1, 'Device1', 0.0)
    await dev1.set_writable()
    dev2 = await myobj.add_variable(2, 'Device2', -10.0)
    await dev2.set_writable()
    dev3 = await myobj.add_variable(3, 'Device3', 100.0)
    await dev3.set_writable()

    _logger.info('Starting server!')
    async with server:
        while True:
            await asyncio.sleep(1)
            dev1_val = await dev1.get_value()
            _logger.info('Set value of %s to %.1f', dev1, dev1_val)
            dev2_val = await dev2.get_value()
            _logger.info('Set value of %s to %.1f', dev2, dev2_val)
            dev3_val = await dev3.get_value()
            _logger.info('Set value of %s to %.1f', dev3, dev3_val)


if __name__ == '__main__':

    logging.basicConfig(level=logging.DEBUG)

    asyncio.run(main(), debug=True)
```


## opc-client

環境変数から以下を指定しています。

| 環境変数    | 値                              | 
| ----------- | ------------------------------- | 
| ACCESS_IP   | OPCサーバーのIPアドレス(接続先) | 
| ACCESS_PORT | OPCサーバーのポート番号(接続先) | 
| NODE_ID     | OPCタグのノードID               | 
| QUARLIFIED_NAME     | OPCタグの修飾名              | 
| EXECUTINON_CYCLE| 書き込む実行周期(ms)              | 
| METHOD_TYPE| 書き込む値(Random/Fluctuation)              | 


```yaml:docker-compose.yml
opc-client-1:
        container_name: opc-client-1
        build:
            context: ./opc-client
            dockerfile: Dockerfile
        tty: true
        environment:
            ACCESS_IP: "192.168.145.25"
            ACCESS_PORT: "4840"
            NODE_ID: "1"
            QUARLIFIED_NAME: "2001"
            EXECUTINON_CYCLE: "3000"
            METHOD_TYPE: "Random"
        volumes:
            - "./opc-client/project:/root/project"
            - "vol:/var/lib/gridstore"
        depends_on:
            - griddb
        networks:
            my_network:
                ipv4_address: 192.168.145.31
```

使ったOPCクライアントライブラリは[eclipse/milo](https://github.com/eclipse/milo)になります。
ライブラリの使用部分は[milo-examples](https://github.com/eclipse/milo/tree/master/milo-examples)と[milo-ece2017/client-examples](https://github.com/ctron/milo-ece2017/tree/master/client-examples)を参考にしました。

App.javaではTimerの定期実行をしてWriteClientService.javaでOPCに書き込んでいます。

```java:App.java
package mototoke.opc.ua.client;

import java.util.Timer;
import java.util.TimerTask;

import java.util.List;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfigBuilder;
import org.eclipse.milo.opcua.stack.client.DiscoveryClient;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription;
import org.eclipse.milo.opcua.stack.core.util.EndpointUtil;

import mototoke.opc.ua.client.services.opcua.BrowseNodeClientService;
import mototoke.opc.ua.client.services.opcua.BrwoseClientService;
import mototoke.opc.ua.client.services.opcua.ReadClientService;
import mototoke.opc.ua.client.services.opcua.ReadNodeClientService;
import mototoke.opc.ua.client.services.opcua.ReadValueClientService;
import mototoke.opc.ua.client.services.opcua.ReadWriteIdClientService;
import mototoke.opc.ua.client.services.opcua.TransferBrowsePathClientService;
import mototoke.opc.ua.client.services.opcua.WriteClientService;

public class App {
    // 環境変数から設定値を取得
    private static final String ip = System.getenv("ACCESS_IP");
    private static final String port = System.getenv("ACCESS_PORT");
    private static final String nodeIdStr = System.getenv("NODE_ID");
    private static final String qualifiedNameStr = System.getenv("QUARLIFIED_NAME");
    private static final String executionCycleStr = System.getenv("EXECUTINON_CYCLE");
    private static final String methodType = System.getenv("METHOD_TYPE");

    public static void main( String[] args )
    {
        Integer executionCycle = Integer.parseInt(executionCycleStr);
        Integer nodeId = Integer.parseInt(nodeIdStr);
        Integer qualifiedName = Integer.parseInt(qualifiedNameStr);

        Timer timer = new Timer();
        TimerTask task = new TimerTask() {
            public void run() {

                try {
                    String endPoint = String.format("opc.tcp://%s:%s/opc-ua/griddb/server", ip, port);
                    List<EndpointDescription> endpoints = DiscoveryClient.getEndpoints(endPoint).get();
                    EndpointDescription configPoint = EndpointUtil.updateUrl(endpoints.get(0), System.getenv("ACCESS_IP"), Integer.parseInt(port));
        
                    OpcUaClientConfigBuilder cfg = new OpcUaClientConfigBuilder();
                    cfg.setEndpoint(configPoint);
        
                    OpcUaClient client = OpcUaClient.create(cfg.build());
                    client.connect().get();
                    
                    // BrwoseClientService clientService = new BrwoseClientService();
                    // BrowseNodeClientService clientService2 = new BrowseNodeClientService();
                    // ReadClientService clientService3 = new ReadClientService();
                    // ReadNodeClientService clientService4 = new ReadNodeClientService();
                    WriteClientService clientService5 = new WriteClientService(new NodeId(nodeId, qualifiedName), methodType);
                    // TransferBrowsePathClientService clientService6 = new TransferBrowsePathClientService();
                    // ReadValueClientService clientService7 = new ReadValueClientService();
                    // ReadWriteIdClientService clientService8 = new ReadWriteIdClientService(new NodeId(2, 2));

                    try {
                        // clientService.run(client);
                        // clientService2.run(client);
                        // clientService3.run(client);
                        // clientService4.run(client);
                        clientService5.run(client);
                        // clientService6.run(client);
                        // clientService7.run(client);
                        // clientService8.run(client);
                    } catch (Exception e1) {
                        e1.printStackTrace();
                    } finally {
                        client.disconnect();
                    }
        
                } catch (Throwable ex) {
                    ex.printStackTrace();
                }
            }
        };
        // 1秒後にexecutionCycleの間隔でtaskを定期実行
        timer.scheduleAtFixedRate(task,1000, executionCycle);
    }
}
```

```java:WriteClientService.java 
package mototoke.opc.ua.client.services.opcua;

import java.util.concurrent.CompletableFuture;

import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode;
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Random;

public class WriteClientService implements IClientBase {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    private NodeId nodeId;

    // MethoType is "Random" or "Fluctuation"
    // Defalut is Flutuation
    private String methodType = "Fluctuation";

    private double writeValue = 0;
    private static double addValue = 0;

    public WriteClientService(NodeId targetNodeId, String type) {
        super();

        this.nodeId = targetNodeId;

        // 処理定義の決定
        switch (type) {
            case "Random":
                this.methodType = type;
                break;
            default:
                this.methodType = "Fluctuation";
                break;
        }
    }

    @Override
    public void run(OpcUaClient client) throws Exception {

        Variant v;

        switch (this.methodType) {
            case "Random":
                // Set Random Value(-1000 ~ 1000)
                v = new Variant(this.randDouble(-1000.0, 1000));
                break;
        
            default:
                // Sin Curve
                this.writeValue = (this.writeValue + WriteClientService.addValue) * 0.1;
                v = new Variant(Math.sin(this.writeValue));
                break;
        }

        WriteClientService.addValue = WriteClientService.addValue + 1;


        DataValue dv = new DataValue(v, null, null);
        CompletableFuture<StatusCode> statusFuture =  client.writeValue(this.nodeId, dv);
        StatusCode status = statusFuture.get();
        if (status.isGood()) {
            logger.info("Wrote '{}' to nodeId={}", v, this.nodeId);
        }
    }

    /**
     * https://stackoverflow.com/questions/40431966/what-is-the-best-way-to-generate-a-random-float-value-included-into-a-specified/51247968
     * @param min Random Range MIN Value
     * @param max Random Range MAX Value
     * @return randome value
     */
    private double randDouble(double min, double max) {
        Random rand = new Random();

        return rand.nextDouble() * (max - min) + min;
    }
}
```

## data-register
このコンテナでOPCタグを定期的に読みに行き、読んだ値をGridDBに登録しています。

ReadValueClientService.java でOPCタグを読み取ります。

```java:ReadValueClientService.java 
package mototoke.opc.ua.griddb.register.services.opcua;

import static java.util.Arrays.asList;
import static java.util.Collections.nCopies;
import static java.util.Collections.singletonList;
import static org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn.Both;

import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

import org.bouncycastle.crypto.tls.CipherType;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.stack.core.AttributeId;
import org.eclipse.milo.opcua.stack.core.Identifiers;
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ReadValueClientService implements IClientBase {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void run(OpcUaClient client) throws Exception {
        // pass
    }

    /**
     * この関数を使う
     * @param <T> 読み取り値の型
     * @param client 
     * @param nodeId 
     * @return
     * @throws ExecutionException
     * @throws InterruptedException
     */
    public <T> T readValue(final OpcUaClient client,
    final NodeId nodeId,
    final Class<T> clazz) throws InterruptedException, ExecutionException {
        DataValue r = this.read(client, nodeId).get();
        Variant value = r.getValue();

        // 値がどんな型か知っている前提
        Object readObject = value.getValue();

        // 指定した型で値を返す
        // 駄目だったときはnull
        return convertInstanceOfObject(readObject, clazz);
    }

    private CompletableFuture<DataValue> read(
            final OpcUaClient client,
            final NodeId nodeId) {

        return client.readValue(0, TimestampsToReturn.Both, nodeId);
    }

    /**
     * https://stackoverflow.com/questions/14524751/cast-object-to-generic-type-for-returning
     * @param <T>
     * @param o
     * @param clazz
     * @return
    */
    private <T> T convertInstanceOfObject(Object o, Class<T> clazz) {
        try {
            return clazz.cast(o);
        } catch(ClassCastException e) {
            return null;
        }
    }
}
```

RegisterService.java で読み取った値をGridDBに入れています。
読み取った値が900以上の場合は付加情報としてステータスを"ERROR"にして登録します。

```java:RegisterService.java 
package mototoke.opc.ua.griddb.register.services.griddb.nosql;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Properties;

import com.toshiba.mwcloud.gs.Collection;
import com.toshiba.mwcloud.gs.GSException;
import com.toshiba.mwcloud.gs.GridStore;
import com.toshiba.mwcloud.gs.GridStoreFactory;
import com.toshiba.mwcloud.gs.Query;
import com.toshiba.mwcloud.gs.RowKey;
import com.toshiba.mwcloud.gs.RowSet;
import com.toshiba.mwcloud.gs.TimeSeries;
import com.toshiba.mwcloud.gs.TimeUnit;
import com.toshiba.mwcloud.gs.TimestampUtils;

import mototoke.opc.ua.griddb.register.entities.griddb.Equip;
import mototoke.opc.ua.griddb.register.entities.griddb.Point;

public class RegisterService {

    private final String equipColName = "equipment_col";
    
    /**
     * constuctor
     */
    public RegisterService() {}

    /**
     * 
     * @param timeSeriesName
     * @param val
     */
    public void insertSensorValue(
        Properties props, String containerName,    
        String timeSeriesName, double val){

        try {
            // Create Gridstore Object
            GridStore store = GridStoreFactory.getInstance().getGridStore(props);
            // Connect Cluster
            store.getContainer(containerName);

            TimeSeries<Point> ts = store.getTimeSeries(timeSeriesName, Point.class);
            Date date = new Date();
            Point point = new Point();
            point.time   = date;
            point.value  = val;

            String status = "";
            if(val > 900) status = "ERROR";
            else status = "NONE";

            point.status = status;
            
            ts.put(date, point);

            store.close();
        } catch (GSException e) {
            e.printStackTrace();
        }
    }

    /**
     * 
     */
    public void debugSelectTimeSeriesValue(Properties props, String containerName, String sensorId){
        try {
            // Create Gridstore Object
            GridStore store = GridStoreFactory.getInstance().getGridStore(props);
            // Connect Cluster
            store.getContainer(containerName);

            // 設備を検索
            Collection<String, Equip> equipCol = store.getCollection(equipColName, Equip.class);
            Equip equip = equipCol.get(sensorId);
            System.out.println("[Equipment] " + equip.name  + " (sensorid) "+ sensorId);

            // 直前の時系列を検索
            String tsName = sensorId;
            TimeSeries<Point> ts = store.getTimeSeries(tsName, Point.class);
            Date endDate   = new Date();
            Date startDate = TimestampUtils.add(endDate, -10, TimeUnit.MINUTE);
            RowSet<Point> rowSet =  ts.query(startDate, endDate).fetch();

            SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.JAPAN);
            while (rowSet.hasNext()) {
                Point ret = rowSet.next();
                System.out.println(
                        "[Result] " +sf.format(ret.time) +
                        " " + ret.value + " " + ret.status);
            }

            store.close();
        } catch (GSException e) {
            e.printStackTrace();
        }
    }
}
```

## 実行
実際に動かしてみます。

```yaml:
git clone https://github.com/mototoke/opc-ua-griddb-docker.git
cd opc-ua-griddb-docker/
docker-compose build
docker-compose up # コンソールの出力を見たいのでdetachはしません
```
全部動かすとターミナルが目まぐるしく動きますが
↓のようにopc-server値が微妙に変化していれば値がOPCサーバーに書き込まれているのが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168779/d269a49f-74bf-be4a-f40a-74468467cba5.png)

また、data-registerがGridDBに書き込んだ後、直近の時系列データを取得し、コンソールに出力しています。
client1,2,3それぞれの値がDBに書き込まれているのが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168779/67934a88-fa8b-7199-bcf9-dffac983fef1.png)

## grafana_plugin
今回は力尽きたのでやっていませんが[GridDBとGrafanaによるデータの視覚化](https://griddb.net/ja/blog/data-visualization-grafana-griddb/)を見ると時系列データを簡単に視覚化できるっぽいです。
一応、Grafanaの起動とID:`admin` PW:`admin`でログインできるところまでは確認しました。
もしGridDB+Grafanaを試すだけなら[griddb-datasource](https://github.com/griddb/griddb-datasource)を参考にすると良さそうです。
誰か続きをやってくだせぇ☺

## まとめ
dockerを使って簡単なopcua+griddbのシステムを試してみました。
exampleを見て実装することが多かったですがライブラリの使い方はちゃんとリファレンスも見ないとだめそうです。
いやしかしdocker使うと環境構築が簡単すぎてもはや怖いですねぇ。

あと、実際にopcクライアントが書き込む値は設備とかPLCから取ってきた値になると思うのでそっちからどうやって値を取得するのかは考えないといけません。
[apache/plc4x](https://github.com/apache/plc4x)というIoT向けのJavaライブラリがあるみたいなのでこれを使うとPLCから簡単に値取ってこれるかも？
これdockerで動かせればドライバとか気にしなくて済むのかな？
知ってる人いたら教えてください。

それと今回はGridDBのCLIも入れましたが[griddb/webapi](https://github.com/griddb/webapi)でもっとお手軽なツールが用意されているみたいです。
う～んdockerって便利。

以上です。

## 参考URL
以下のURLを参考にしました。感謝感謝。

[https://qiita.com/s5uishida/items/60954d356a3ea1048c61](https://qiita.com/s5uishida/items/60954d356a3ea1048c61)
[https://www.toshiba-sol.co.jp/pro/griddb/docs-jp/v4_3/GridDB_ProgrammingGuide.html](https://www.toshiba-sol.co.jp/pro/griddb/docs-jp/v4_3/GridDB_ProgrammingGuide.html)
[https://github.com/griddb/griddb-datasource](https://github.com/griddb/griddb-datasource)
[https://github.com/griddb/webapi](https://github.com/griddb/webapi)
[https://github.com/FreeOpcUa/opcua-asyncio](https://github.com/FreeOpcUa/opcua-asyncio)