はじめに
docker環境で設備から取得した値をopcuaに集約し、定周期でGridDBに蓄積する超簡単なシステムを考えてみます。
(設備はないので適当な値をopcタグに書き込みます)
イメージは↓の感じです。
GridDB プログラミングチュートリアルを自分なりにアレンジ加えた感じです。
家でお勉強用にちゃちゃっとやったやつなので全体的に適当です。
またもや自分の備忘録ですね。
sample
mototoke/opc-ua-griddb-dockerにソース一式まとめてます。
docker-compose.yml
環境変数とかdockerネットワークとか書いてたらけっこうデカめのymlになりました。
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
griddb
ほとんど参考URLの通りです。
Dockerfileはgriddb-cliのインストールを追加しています。
# データアップローダー用の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
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のserver-minimal.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) |
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になります。
ライブラリの使用部分はmilo-examplesとmilo-ece2017/client-examplesを参考にしました。
App.javaではTimerの定期実行をしてWriteClientService.javaでOPCに書き込んでいます。
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);
}
}
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タグを読み取ります。
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"にして登録します。
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();
}
}
}
実行
実際に動かしてみます。
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サーバーに書き込まれているのが確認できます。
また、data-registerがGridDBに書き込んだ後、直近の時系列データを取得し、コンソールに出力しています。
client1,2,3それぞれの値がDBに書き込まれているのが確認できます。
grafana_plugin
今回は力尽きたのでやっていませんがGridDBとGrafanaによるデータの視覚化を見ると時系列データを簡単に視覚化できるっぽいです。
一応、Grafanaの起動とID:admin
PW:admin
でログインできるところまでは確認しました。
もしGridDB+Grafanaを試すだけならgriddb-datasourceを参考にすると良さそうです。
誰か続きをやってくだせぇ☺
まとめ
dockerを使って簡単なopcua+griddbのシステムを試してみました。
exampleを見て実装することが多かったですがライブラリの使い方はちゃんとリファレンスも見ないとだめそうです。
いやしかしdocker使うと環境構築が簡単すぎてもはや怖いですねぇ。
あと、実際にopcクライアントが書き込む値は設備とかPLCから取ってきた値になると思うのでそっちからどうやって値を取得するのかは考えないといけません。
apache/plc4xというIoT向けのJavaライブラリがあるみたいなのでこれを使うとPLCから簡単に値取ってこれるかも?
これdockerで動かせればドライバとか気にしなくて済むのかな?
知ってる人いたら教えてください。
それと今回はGridDBのCLIも入れましたがgriddb/webapiでもっとお手軽なツールが用意されているみたいです。
う~んdockerって便利。
以上です。
参考URL
以下のURLを参考にしました。感謝感謝。
https://qiita.com/s5uishida/items/60954d356a3ea1048c61
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/webapi
https://github.com/FreeOpcUa/opcua-asyncio