diff --git a/NOTICE b/NOTICE index 97048d6e923..6ff3737bfe3 100644 --- a/NOTICE +++ b/NOTICE @@ -8,3 +8,6 @@ The Apache Software Foundation (http://www.apache.org/). This product includes software developed at The Netty project (https://netty.io/). + +This product includes software developed at +The Milo project (https://github.com/eclipse/milo). diff --git a/plc4j/drivers/opcua/pom.xml b/plc4j/drivers/opcua/pom.xml new file mode 100644 index 00000000000..f0f47df59b8 --- /dev/null +++ b/plc4j/drivers/opcua/pom.xml @@ -0,0 +1,86 @@ + + + + + 4.0.0 + + + org.apache.plc4x + plc4j-drivers + 0.4.0-SNAPSHOT + + + plc4j-driver-opcua + PLC4J: Driver: OPC UA + Implementation of a PLC4X driver able to speak with devices using the OPC UA protocol. + + + + org.apache.plc4x + plc4j-api + 0.4.0-SNAPSHOT + + + + org.apache.plc4x + plc4j-protocol-driver-base + 0.4.0-SNAPSHOT + + + + org.eclipse.milo + sdk-client + 0.3.0-M1 + + + org.eclipse.milo + stack-core + 0.3.0-M1 + + + org.eclipse.milo + stack-client + 0.3.0-M1 + + + + org.apache.commons + commons-lang3 + + + + + + + \ No newline at end of file diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaPlcDriver.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaPlcDriver.java new file mode 100644 index 00000000000..7fefae7c45a --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaPlcDriver.java @@ -0,0 +1,94 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.plc4x.java.opcua; + +import org.apache.commons.lang3.StringUtils; +import org.apache.plc4x.java.api.PlcConnection; +import org.apache.plc4x.java.api.authentication.PlcAuthentication; +import org.apache.plc4x.java.api.exceptions.PlcConnectionException; +import org.apache.plc4x.java.opcua.connection.OpcuaConnectionFactory; +import org.apache.plc4x.java.spi.PlcDriver; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +/** + * Implementation of the OPC UA protocol, based on: + * - Eclipse Milo (https://github.com/eclipse/milo) + * + * @author Matthias Milan Stlrljic + * Created by Matthias Milan Stlrljic on 10.05.2019 + */ +public class OpcuaPlcDriver implements PlcDriver { + + + + public static final Pattern INET_ADDRESS_PATTERN = Pattern.compile("tcp://(?[\\w.-]+)(:(?\\d*))?"); + public static final Pattern OPCUA_URI_PATTERN = Pattern.compile("^opcua:(" + INET_ADDRESS_PATTERN + ")?" + "(?/[\\w/]+)?"); + private static final int requestTimeout = 10000; + private OpcuaConnectionFactory opcuaConnectionFactory; + + public OpcuaPlcDriver() { + this.opcuaConnectionFactory = new OpcuaConnectionFactory(); + } + + public OpcuaPlcDriver(OpcuaConnectionFactory opcuaConnectionFactory) { + this.opcuaConnectionFactory = opcuaConnectionFactory; + } + + @Override + public String getProtocolCode() { + return "opcua"; + } + + @Override + public String getProtocolName() { + return "OPC UA (TCP)"; + } + + @Override + public PlcConnection connect(String url) throws PlcConnectionException { + Matcher matcher = OPCUA_URI_PATTERN.matcher(url); + + if (!matcher.matches()) { + throw new PlcConnectionException( + "Connection url doesn't match the format 'opcua:{type}//{port|host}'"); + } + + String host = matcher.group("host"); + String portString = matcher.group("port"); + Integer port = StringUtils.isNotBlank(portString) ? Integer.parseInt(portString) : null; + String params = matcher.group("params") != null ? matcher.group("params").substring(1) : null; + + try { + return opcuaConnectionFactory.opcuaTcpPlcConnectionOf(InetAddress.getByName(host), port, params, requestTimeout); + } catch (UnknownHostException e) { + throw new PlcConnectionException(e); + } + } + + @Override + public PlcConnection connect(String url, PlcAuthentication authentication) throws PlcConnectionException { + throw new PlcConnectionException("opcua does not support Auth at this state"); + } + +} diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/BaseOpcuaPlcConnection.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/BaseOpcuaPlcConnection.java new file mode 100644 index 00000000000..50a5bd0633c --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/BaseOpcuaPlcConnection.java @@ -0,0 +1,95 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.plc4x.java.opcua.connection; + +import org.apache.commons.lang3.StringUtils; +import org.apache.plc4x.java.api.messages.PlcReadRequest; +import org.apache.plc4x.java.api.messages.PlcSubscriptionRequest; +import org.apache.plc4x.java.api.messages.PlcUnsubscriptionRequest; +import org.apache.plc4x.java.api.messages.PlcWriteRequest; +import org.apache.plc4x.java.base.connection.AbstractPlcConnection; +import org.apache.plc4x.java.base.messages.*; +import org.apache.plc4x.java.opcua.protocol.OpcuaPlcFieldHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Matthias Milan Stlrljic + * Created by Matthias Milan Stlrljic on 10.05.2019 + */ +public abstract class BaseOpcuaPlcConnection extends AbstractPlcConnection implements PlcReader, PlcWriter, PlcSubscriber { + + private static final Logger logger = LoggerFactory.getLogger(BaseOpcuaPlcConnection.class); + + BaseOpcuaPlcConnection(String params) { + + if (!StringUtils.isEmpty(params)) { + for (String param : params.split("&")) { + String[] paramElements = param.split("="); + String paramName = paramElements[0]; + if (paramElements.length == 2) { + String paramValue = paramElements[1]; + switch (paramName) { + default: + logger.debug("Unknown parameter {} with value {}", paramName, paramValue); + } + } else { + logger.debug("Unknown no-value parameter {}", paramName); + } + } + } + } + + @Override + public boolean canRead() { + return true; + } + + @Override + public boolean canWrite() { + return true; + } + + @Override + public PlcReadRequest.Builder readRequestBuilder() { + return new DefaultPlcReadRequest.Builder(this, new OpcuaPlcFieldHandler()); + } + + @Override + public PlcWriteRequest.Builder writeRequestBuilder() { + return new DefaultPlcWriteRequest.Builder(this, new OpcuaPlcFieldHandler()); + } + + @Override + public boolean canSubscribe() { + return true; + } + + @Override + public PlcSubscriptionRequest.Builder subscriptionRequestBuilder() { + return new DefaultPlcSubscriptionRequest.Builder(this, new OpcuaPlcFieldHandler()); + } + + @Override + public PlcUnsubscriptionRequest.Builder unsubscriptionRequestBuilder() { + return new DefaultPlcUnsubscriptionRequest.Builder(this); + } + + +} diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/OpcuaConnectionFactory.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/OpcuaConnectionFactory.java new file mode 100644 index 00000000000..5c399f30843 --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/OpcuaConnectionFactory.java @@ -0,0 +1,39 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.plc4x.java.opcua.connection; + +import java.net.InetAddress; +import java.util.Objects; +/** + * @author Matthias Milan Stlrljic + * Created by Matthias Milan Stlrljic on 10.05.2019 + */ +public class OpcuaConnectionFactory { + + public OpcuaTcpPlcConnection opcuaTcpPlcConnectionOf(InetAddress address, Integer port, String params, int requestTimeout) { + Objects.requireNonNull(address); + + if (port == null) { + return OpcuaTcpPlcConnection.of(address, params, requestTimeout); + } else { + return OpcuaTcpPlcConnection.of(address, port, params, requestTimeout); + } + } + +} diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/OpcuaTcpPlcConnection.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/OpcuaTcpPlcConnection.java new file mode 100644 index 00000000000..c59c36a35ea --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/OpcuaTcpPlcConnection.java @@ -0,0 +1,464 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + * @author Matthias Milan Stlrljic + * Created by Matthias Milan Stlrljic on 10.05.2019 + */ +package org.apache.plc4x.java.opcua.connection; + +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.plc4x.java.api.exceptions.PlcConnectionException; +import org.apache.plc4x.java.api.messages.*; +import org.apache.plc4x.java.api.model.PlcConsumerRegistration; +import org.apache.plc4x.java.api.model.PlcField; +import org.apache.plc4x.java.api.model.PlcSubscriptionHandle; +import org.apache.plc4x.java.api.types.PlcResponseCode; +import org.apache.plc4x.java.base.messages.*; +import org.apache.plc4x.java.base.messages.items.*; +import org.apache.plc4x.java.base.model.SubscriptionPlcField; +import org.apache.plc4x.java.opcua.protocol.OpcuaField; +import org.apache.plc4x.java.opcua.protocol.OpcuaSubsriptionHandle; +import org.eclipse.milo.opcua.sdk.client.OpcUaClient; +import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfig; +import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider; +import org.eclipse.milo.opcua.sdk.client.api.identity.IdentityProvider; +import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaMonitoredItem; +import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaSubscription; +import org.eclipse.milo.opcua.stack.client.DiscoveryClient; +import org.eclipse.milo.opcua.stack.core.AttributeId; +import org.eclipse.milo.opcua.stack.core.Identifiers; +import org.eclipse.milo.opcua.stack.core.UaException; +import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy; +import org.eclipse.milo.opcua.stack.core.types.builtin.*; +import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; +import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort; +import org.eclipse.milo.opcua.stack.core.types.enumerated.MonitoringMode; +import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn; +import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription; +import org.eclipse.milo.opcua.stack.core.types.structured.MonitoredItemCreateRequest; +import org.eclipse.milo.opcua.stack.core.types.structured.MonitoringParameters; +import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigInteger; +import java.net.InetAddress; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint; + +public class OpcuaTcpPlcConnection extends BaseOpcuaPlcConnection { + + private static final int OPCUA_DEFAULT_TCP_PORT = 4840; + + private static final Logger logger = LoggerFactory.getLogger(OpcuaTcpPlcConnection.class); + private InetAddress address; + private int requestTimeout = 5000; + private int port; + private String params; + private OpcUaClient client; + private boolean isConnected = false; + private final AtomicLong clientHandles = new AtomicLong(1L); + + private OpcuaTcpPlcConnection(InetAddress address, String params, int requestTimeout) { + this( address, OPCUA_DEFAULT_TCP_PORT, params, requestTimeout); + logger.info("Configured OpcuaTcpPlcConnection with: host-name {}", address.getHostAddress()); + } + + public OpcuaTcpPlcConnection(InetAddress address, int port, String params, int requestTimeout) { + this(params); + logger.info("Configured OpcuaTcpPlcConnection with: host-name {}", address.getHostAddress()); + this.address = address; + this.port = port; + this.params = params; + this.requestTimeout = requestTimeout; + } + + public OpcuaTcpPlcConnection(String params) { + super(params); + } + + public static OpcuaTcpPlcConnection of(InetAddress address, String params, int requestTimeout) { + return new OpcuaTcpPlcConnection(address, params, requestTimeout); + } + + public static OpcuaTcpPlcConnection of(InetAddress address, int port, String params, int requestTimeout) { + return new OpcuaTcpPlcConnection(address, port, params, requestTimeout); + } + + public static BaseDefaultFieldItem encodeFieldItem(DataValue value){ + NodeId typeNode = value.getValue().getDataType().get(); + Object objValue = value.getValue().getValue(); + + if(typeNode.equals(Identifiers.Boolean)){ + return new DefaultBooleanFieldItem((Boolean)objValue); + }else if (typeNode.equals(Identifiers.ByteString)){ + byte[] array = ((ByteString)objValue).bytes(); + Byte[] byteArry = new Byte[array.length]; + int counter = 0; + for (byte bytie: array + ) { + byteArry[counter] = bytie; + counter++; + } + return new DefaultByteArrayFieldItem(byteArry); + }else if (typeNode.equals(Identifiers.Integer)){ + return new DefaultIntegerFieldItem((Integer)objValue); + }else if (typeNode.equals(Identifiers.Int16)){ + return new DefaultShortFieldItem((Short)objValue); + }else if (typeNode.equals(Identifiers.Int32)){ + return new DefaultIntegerFieldItem((Integer)objValue); + }else if (typeNode.equals(Identifiers.Int64)){ + return new DefaultLongFieldItem((Long)objValue); + }else if (typeNode.equals(Identifiers.UInteger)){ + return new DefaultLongFieldItem((Long)objValue); + }else if (typeNode.equals(Identifiers.UInt16)){ + return new DefaultIntegerFieldItem(((UShort)objValue).intValue()); + }else if (typeNode.equals(Identifiers.UInt32)){ + return new DefaultLongFieldItem(((UInteger)objValue).longValue()); + }else if (typeNode.equals(Identifiers.UInt64)){ + return new DefaultBigIntegerFieldItem(new BigInteger(objValue.toString())); + }else if (typeNode.equals(Identifiers.Byte)){ + return new DefaultShortFieldItem(Short.valueOf(objValue.toString())); + }else if (typeNode.equals(Identifiers.Float)){ + return new DefaultFloatFieldItem((Float)objValue); + }else if (typeNode.equals(Identifiers.Double)){ + return new DefaultDoubleFieldItem((Double)objValue); + }else if (typeNode.equals(Identifiers.SByte)){ + return new DefaultByteFieldItem((Byte)objValue); + }else { + return new DefaultStringFieldItem(objValue.toString()); + } + + } + + public InetAddress getRemoteAddress() { + return address; + } + + @Override + public void connect() throws PlcConnectionException { + List endpoints = null; + + try { + endpoints = DiscoveryClient.getEndpoints(getEndpointUrl(address, port, params)).get(); + //TODO Exception should be handeled better when the Discovery-API of Milo is stable + } catch (Exception ex) { + // try the explicit discovery endpoint as well + String discoveryUrl = getEndpointUrl(address, port, params); + + if (!discoveryUrl.endsWith("/")) { + discoveryUrl += "/"; + } + discoveryUrl += "discovery"; + + logger.info("Trying explicit discovery URL: {}", discoveryUrl); + try { + endpoints = DiscoveryClient.getEndpoints(discoveryUrl).get(); + } catch (InterruptedException | ExecutionException e) { + throw new PlcConnectionException("Unable to discover URL:" + discoveryUrl); + } + } + + EndpointDescription endpoint = endpoints.stream() + .filter(e -> e.getSecurityPolicyUri().equals(getSecurityPolicy().getUri())) + .filter(endpointFilter()) + .findFirst() + .orElseThrow(() -> new PlcConnectionException("No desired endpoints from")); + + OpcUaClientConfig config = OpcUaClientConfig.builder() + .setApplicationName(LocalizedText.english("eclipse milo opc-ua client of the apache PLC4X:PLC4J project")) + .setApplicationUri("urn:eclipse:milo:plc4x:client") + .setEndpoint(endpoint) + .setIdentityProvider(getIdentityProvider()) + .setRequestTimeout(UInteger.valueOf(requestTimeout)) + .build(); + + try { + this.client = OpcUaClient.create(config); + this.client.connect().get(); + isConnected = true; + } catch (UaException e) { + isConnected = false; + String message = (config == null) ? "NULL" : config.toString(); + throw new PlcConnectionException("The given input values are a not valid OPC UA connection configuration [CONFIG]: " + message); + } catch (InterruptedException | ExecutionException e) { + isConnected = false; + throw new PlcConnectionException("Error while creation of the connection because of : " + e.getMessage()); + } + } + + @Override + public boolean isConnected() { + return client != null && isConnected; + } + + @Override + public void close() throws Exception { + if(client != null){ + client.disconnect().get(); + isConnected = false; + } + } + + @Override + public CompletableFuture subscribe(PlcSubscriptionRequest subscriptionRequest) { + InternalPlcSubscriptionRequest internalPlcSubscriptionRequest = checkInternal(subscriptionRequest, InternalPlcSubscriptionRequest.class); + CompletableFuture future = CompletableFuture.supplyAsync(() ->{ + Map> responseItems = internalPlcSubscriptionRequest.getSubscriptionPlcFieldMap().entrySet().stream() + .map(subscriptionPlcFieldEntry -> { + final String plcFieldName = subscriptionPlcFieldEntry.getKey(); + final SubscriptionPlcField subscriptionPlcField = subscriptionPlcFieldEntry.getValue(); + final OpcuaField field = (OpcuaField)Objects.requireNonNull(subscriptionPlcField.getPlcField()); + long cycleTime = subscriptionPlcField.getDuration().orElse(Duration.ofSeconds(1)).toMillis(); + NodeId idNode = generateNodeId(field); + ReadValueId readValueId = new ReadValueId( + idNode, + AttributeId.Value.uid(), null, QualifiedName.NULL_VALUE); + UInteger clientHandle = uint(clientHandles.getAndIncrement()); + + MonitoringParameters parameters = new MonitoringParameters( + clientHandle, + (double) cycleTime, // sampling interval + null, // filter, null means use default + uint(1), // queue size + true // discard oldest + ); + MonitoringMode monitoringMode; + switch (subscriptionPlcField.getPlcSubscriptionType()) { + case CYCLIC: + monitoringMode = MonitoringMode.Sampling; + break; + case CHANGE_OF_STATE: + monitoringMode = MonitoringMode.Reporting; + break; + case EVENT: + monitoringMode = MonitoringMode.Reporting; + break; + default: monitoringMode = MonitoringMode.Reporting; + } + + PlcSubscriptionHandle subHandle = null; + PlcResponseCode responseCode = PlcResponseCode.ACCESS_DENIED; + try { + UaSubscription subscription = client.getSubscriptionManager().createSubscription(1000.0).get(); + + MonitoredItemCreateRequest request = new MonitoredItemCreateRequest( + readValueId, monitoringMode, parameters); + List requestList = new LinkedList<>(); + requestList.add(request); + OpcuaSubsriptionHandle subsriptionHandle = new OpcuaSubsriptionHandle(plcFieldName, clientHandle); + BiConsumer onItemCreated = + (item, id) -> item.setValueConsumer(subsriptionHandle::onSubscriptionValue); + + List items = subscription.createMonitoredItems( + TimestampsToReturn.Both, + requestList, + onItemCreated + ).get(); + + subHandle = subsriptionHandle; + responseCode = PlcResponseCode.OK; + } catch (InterruptedException | ExecutionException e) { + logger.warn("Unable to subscribe Elements because of: {}", e.getMessage()); + } + + + return Pair.of(plcFieldName, Pair.of(responseCode, subHandle)); + }) + .collect(Collectors.toMap(Pair::getKey, Pair::getValue)); + return (PlcSubscriptionResponse) new DefaultPlcSubscriptionResponse(internalPlcSubscriptionRequest, responseItems); + }); + + return future; + } + + @Override + public CompletableFuture unsubscribe(PlcUnsubscriptionRequest unsubscriptionRequest) { + InternalPlcUnsubscriptionRequest internalPlcUnsubscriptionRequest = checkInternal(unsubscriptionRequest, InternalPlcUnsubscriptionRequest.class); + internalPlcUnsubscriptionRequest.getInternalPlcSubscriptionHandles().forEach(o -> { + OpcuaSubsriptionHandle opcSubHandle = (OpcuaSubsriptionHandle) o; + try { + client.getSubscriptionManager().deleteSubscription(opcSubHandle.getClientHandle()).get(); + } catch (InterruptedException | ExecutionException e) { + logger.warn("Unable to unsubscribe Elements because of: {}", e.getMessage()); + } + }); + + return null; + } + + @Override + public PlcConsumerRegistration register(Consumer consumer, Collection handles) { + List unregisters = new LinkedList<>(); + handles.forEach(plcSubscriptionHandle -> unregisters.add(plcSubscriptionHandle.register(consumer))); + + return () -> unregisters.forEach(PlcConsumerRegistration::unregister); + } + + @Override + public void unregister(PlcConsumerRegistration registration) { + registration.unregister(); + } + + @Override + public CompletableFuture read(PlcReadRequest readRequest) { + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + readRequest.getFields(); + Map> fields = new HashMap<>(); + List readValueIds = new LinkedList<>(); + List readPLCValues = readRequest.getFields(); + for (PlcField field: readPLCValues) { + NodeId idNode = generateNodeId((OpcuaField) field); + readValueIds.add(idNode); + } + + CompletableFuture> dataValueCompletableFuture = client.readValues(0.0, TimestampsToReturn.Both, readValueIds); + List readValues = null; + try { + readValues = dataValueCompletableFuture.get(); + } catch (InterruptedException | ExecutionException e) { + logger.warn("Unable to read Elements because of: {}", e.getMessage()); + } + for(int counter = 0; counter < readValueIds.size(); counter++){ + PlcResponseCode resultCode = PlcResponseCode.OK; + BaseDefaultFieldItem stringItem = null; + if(readValues == null || readValues.size() <= counter || readValues.get(counter).getStatusCode() != StatusCode.GOOD){ + resultCode = PlcResponseCode.NOT_FOUND; + }else{ + stringItem = encodeFieldItem(readValues.get(counter)); + + } + Pair newPair = new ImmutablePair<>(resultCode, stringItem); + fields.put((String) readRequest.getFieldNames().toArray()[counter], newPair); + + + } + InternalPlcReadRequest internalPlcReadRequest = checkInternal(readRequest, InternalPlcReadRequest.class); + return (PlcReadResponse) new DefaultPlcReadResponse(internalPlcReadRequest, fields ); + }); + + + return future; + } + + + @Override + public CompletableFuture write(PlcWriteRequest writeRequest) { + CompletableFuture future; + future = CompletableFuture.supplyAsync(() -> { + + InternalPlcWriteRequest internalPlcWriteRequest = (InternalPlcWriteRequest) writeRequest; + + List writePLCValues = writeRequest.getFields(); + LinkedList values = new LinkedList<>(); + LinkedList ids = new LinkedList<>(); + LinkedList names = new LinkedList<>(); + Map fieldResponse = new HashMap<>(); + for (String fieldName: writeRequest.getFieldNames()) { + OpcuaField uaField = (OpcuaField) writeRequest.getField(fieldName); + NodeId idNode = generateNodeId(uaField); + Variant var = new Variant(internalPlcWriteRequest.getFieldItem(fieldName).getObject(0)); + DataValue value = new DataValue(var, null, null); + ids.add(idNode); + names.add(fieldName); + values.add(value); + } + CompletableFuture> opcRequest = + client.writeValues(ids, values); + List statusCodes = null; + try { + statusCodes = opcRequest.get(); + } catch (InterruptedException | ExecutionException e) { + statusCodes = new LinkedList<>(); + for(int counter = 0; counter < ids.size(); counter++){ + ((LinkedList) statusCodes).push(StatusCode.BAD); + } + } + + for(int counter = 0; counter < names.size(); counter++){ + PlcResponseCode resultCode; + if(statusCodes != null && statusCodes.size() > counter){ + if(statusCodes.get(counter).isGood()){ + resultCode = PlcResponseCode.OK; + }else if(statusCodes.get(counter).isUncertain()){ + resultCode = PlcResponseCode.NOT_FOUND; + }else { + resultCode = PlcResponseCode.ACCESS_DENIED; + } + }else{ + resultCode = PlcResponseCode.ACCESS_DENIED; + } + fieldResponse.put(names.get(counter), resultCode); + } + InternalPlcWriteRequest internalPlcReadRequest = checkInternal(writeRequest, InternalPlcWriteRequest.class); + PlcWriteResponse response = new DefaultPlcWriteResponse(internalPlcReadRequest, fieldResponse); + return response; + }); + + + return future; + } + + + private NodeId generateNodeId(OpcuaField uaField){ + NodeId idNode = null; + switch (uaField.getIdentifierType()) { + case STRING_IDENTIFIER: + idNode = new NodeId(uaField.getNamespace(), uaField.getIdentifier()); + break; + case NUMBER_IDENTIFIER: + idNode = new NodeId(uaField.getNamespace(), UInteger.valueOf(uaField.getIdentifier())); + break; + case GUID_IDENTIFIER: + idNode = new NodeId(uaField.getNamespace(), UUID.fromString(uaField.getIdentifier())); + break; + case BINARY_IDENTIFIER: + idNode = new NodeId(uaField.getNamespace(), new ByteString(uaField.getIdentifier().getBytes())); + break; + + default: idNode = new NodeId(uaField.getNamespace(), uaField.getIdentifier()); + } + + return idNode; + } + + private String getEndpointUrl(InetAddress address, Integer port, String params) { + return "opc.tcp://" + address.getHostAddress() +":" + port + "/" + params; + } + + private Predicate endpointFilter() { + return e -> true; + } + + private SecurityPolicy getSecurityPolicy() { + return SecurityPolicy.None; + } + + private IdentityProvider getIdentityProvider() { + return new AnonymousProvider(); + } +} diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaField.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaField.java new file mode 100644 index 00000000000..91ece25e682 --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaField.java @@ -0,0 +1,118 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +package org.apache.plc4x.java.opcua.protocol; + +import org.apache.plc4x.java.api.exceptions.PlcInvalidFieldException; +import org.apache.plc4x.java.api.model.PlcField; +import org.apache.plc4x.java.opcua.protocol.model.OpcuaIdentifierType; + +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +/** + * @author Matthias Milan Stlrljic + * Created by Matthias Milan Stlrljic on 10.05.2019 + */ +public class OpcuaField implements PlcField { + + public static final Pattern ADDRESS_PATTERN = Pattern.compile("^ns=(?\\d+);(?[isgb])=((?\\w+))?"); + + private final OpcuaIdentifierType identifierType; + + private final int namespace; + + private final String identifier; + + protected OpcuaField(int namespace, OpcuaIdentifierType identifierType, String identifier) { + this.namespace = namespace; + this.identifier = identifier; + this.identifierType = identifierType; + if (this.identifier == null || this.namespace < 0) { + throw new IllegalArgumentException("Identifier can not be null or Namespace can not be lower then 0."); + } + } + + private OpcuaField(Integer namespace, String identifier, OpcuaIdentifierType identifierType) { + this.identifier = Objects.requireNonNull(identifier); + this.identifierType = Objects.requireNonNull(identifierType); + this.namespace = namespace != null ? namespace : 0; + if (this.namespace < 0) { + throw new IllegalArgumentException("namespace must be greater then zero. Was " + this.namespace); + } + } + + public static OpcuaField of(String address) { + Matcher matcher = ADDRESS_PATTERN.matcher(address); + if (!matcher.matches()) { + throw new PlcInvalidFieldException(address, ADDRESS_PATTERN, "{address}"); + } + String identifier = matcher.group("identifier"); + + String identifierTypeString = matcher.group("identifierType"); + OpcuaIdentifierType identifierType = OpcuaIdentifierType.fromString(identifierTypeString); + + String namespaceString = matcher.group("namespace"); + Integer namespace = namespaceString != null ? Integer.valueOf(namespaceString) : 0; + + return new OpcuaField(namespace, identifier, identifierType); + } + + + public static boolean matches(String address) { + return ADDRESS_PATTERN.matcher(address).matches(); + } + + public int getNamespace() { + return namespace; + } + + public String getIdentifier() { + return identifier; + } + + public OpcuaIdentifierType getIdentifierType() { + return identifierType; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof OpcuaField)) { + return false; + } + OpcuaField that = (OpcuaField) o; + return namespace == that.namespace && identifier.equals(that.identifier) && identifierType == that.identifierType; + } + + @Override + public int hashCode() { + return Objects.hash(namespace); + } + + @Override + public String toString() { + return "OpcuaField{" + + "namespace=" + namespace + + "identifierType=" + identifierType.getText() + + "identifier=" + identifier + + '}'; + } +} diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaPlcFieldHandler.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaPlcFieldHandler.java new file mode 100644 index 00000000000..bb44a8e9a75 --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaPlcFieldHandler.java @@ -0,0 +1,151 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.plc4x.java.opcua.protocol; + + +import org.apache.plc4x.java.api.exceptions.PlcInvalidFieldException; +import org.apache.plc4x.java.api.model.PlcField; +import org.apache.plc4x.java.base.connection.DefaultPlcFieldHandler; +import org.apache.plc4x.java.base.messages.items.*; + +import java.math.BigInteger; +import java.util.ArrayList; +/** + * @author Matthias Milan Stlrljic + * Created by Matthias Milan Stlrljic on 10.05.2019 + */ +public class OpcuaPlcFieldHandler extends DefaultPlcFieldHandler { + + @Override + public PlcField createField(String fieldQuery) throws PlcInvalidFieldException { + if (OpcuaField.matches(fieldQuery)) { + return OpcuaField.of(fieldQuery); + } + throw new PlcInvalidFieldException(fieldQuery); + } + + @Override + public BaseDefaultFieldItem encodeString(PlcField field, Object[] values) { + OpcuaField adsField = (OpcuaField) field; + ArrayList resultSet = new ArrayList<>(); + for(Object item : values){ + resultSet.add(item.toString()); + } + return new DefaultStringFieldItem(resultSet.toArray(new String[0])); + } + + @Override + public BaseDefaultFieldItem encodeBoolean(PlcField field, Object[] values) { + OpcuaField adsField = (OpcuaField) field; + ArrayList resultSet = new ArrayList<>(); + for(Object item : values){ + resultSet.add((Boolean) item); + } + return new DefaultBooleanFieldItem(resultSet.toArray(new Boolean[0])); + } + + @Override + public BaseDefaultFieldItem encodeByte(PlcField field, Object[] values) { + OpcuaField adsField = (OpcuaField) field; + ArrayList resultSet = new ArrayList<>(); + for(Object item : values){ + resultSet.add((Byte) item); + } + return new DefaultByteFieldItem(resultSet.toArray(new Byte[0])); + } + + @Override + public BaseDefaultFieldItem encodeShort(PlcField field, Object[] values) { + OpcuaField adsField = (OpcuaField) field; + ArrayList resultSet = new ArrayList<>(); + for(Object item : values){ + resultSet.add((Short) item); + } + return new DefaultShortFieldItem(resultSet.toArray(new Short[0])); + } + + @Override + public BaseDefaultFieldItem encodeInteger(PlcField field, Object[] values) { + OpcuaField adsField = (OpcuaField) field; + ArrayList resultSet = new ArrayList<>(); + for(Object item : values){ + resultSet.add((Integer) item); + } + return new DefaultIntegerFieldItem(resultSet.toArray(new Integer[0])); + } + + @Override + public BaseDefaultFieldItem encodeBigInteger(PlcField field, Object[] values) { + OpcuaField adsField = (OpcuaField) field; + ArrayList resultSet = new ArrayList<>(); + for(Object item : values){ + resultSet.add((BigInteger) item); + } + return new DefaultBigIntegerFieldItem(resultSet.toArray(new BigInteger[0])); + } + + @Override + public BaseDefaultFieldItem encodeLong(PlcField field, Object[] values) { + OpcuaField adsField = (OpcuaField) field; + ArrayList resultSet = new ArrayList<>(); + for(Object item : values){ + resultSet.add((Long) item); + } + return new DefaultLongFieldItem(resultSet.toArray(new Long[0])); + } + + @Override + public BaseDefaultFieldItem encodeFloat(PlcField field, Object[] values) { + OpcuaField adsField = (OpcuaField) field; + ArrayList resultSet = new ArrayList<>(); + for(Object item : values){ + resultSet.add((Float) item); + } + return new DefaultFloatFieldItem(resultSet.toArray(new Float[0])); + } + + + + @Override + public BaseDefaultFieldItem encodeDouble(PlcField field, Object[] values) { + OpcuaField adsField = (OpcuaField) field; + ArrayList resultSet = new ArrayList<>(); + for(Object item : values){ + resultSet.add((Double) item); + } + return new DefaultDoubleFieldItem(resultSet.toArray(new Double[0])); + } + + + @Override + public BaseDefaultFieldItem encodeByteArray(PlcField field, Object[] values) { + OpcuaField adsField = (OpcuaField) field; + Byte[][] byteArray = new Byte[values.length][]; + int innerCounter = 0; + for(Object item : values){ + byte[] itemArray = (byte[]) item; + byteArray[innerCounter] = new Byte[((byte[]) item).length]; + for(int counter = 0; counter < itemArray.length; counter++){ + byteArray[innerCounter][counter] = itemArray[counter]; + } + innerCounter++; + } + return new DefaultByteArrayFieldItem(byteArray); + } +} diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubsriptionHandle.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubsriptionHandle.java new file mode 100644 index 00000000000..8080d2209c5 --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubsriptionHandle.java @@ -0,0 +1,81 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.plc4x.java.opcua.protocol; + +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.plc4x.java.api.messages.PlcSubscriptionEvent; +import org.apache.plc4x.java.api.model.PlcConsumerRegistration; +import org.apache.plc4x.java.api.model.PlcSubscriptionHandle; +import org.apache.plc4x.java.api.types.PlcResponseCode; +import org.apache.plc4x.java.base.messages.DefaultPlcSubscriptionEvent; +import org.apache.plc4x.java.base.messages.items.BaseDefaultFieldItem; +import org.apache.plc4x.java.opcua.connection.OpcuaTcpPlcConnection; +import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaMonitoredItem; +import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue; +import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode; +import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; + +import java.time.Instant; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +/** + * @author Matthias Milan Stlrljic + * Created by Matthias Milan Stlrljic on 10.05.2019 + */ +public class OpcuaSubsriptionHandle implements PlcSubscriptionHandle { + Set< Consumer> consumers = new HashSet<>(); + String fieldName; + public UInteger getClientHandle() { + return clientHandle; + } + + UInteger clientHandle; + + public OpcuaSubsriptionHandle(String fieldName, UInteger clientHandle){ + this.clientHandle = clientHandle; + } + @Override + public PlcConsumerRegistration register(Consumer consumer) { + consumers.add(consumer); + return () -> {consumers.remove(consumer);}; + } + + public void onSubscriptionValue(UaMonitoredItem item, DataValue value) { + consumers.forEach(plcSubscriptionEventConsumer -> { + PlcResponseCode resultCode = PlcResponseCode.OK; + BaseDefaultFieldItem stringItem = null; + if(value.getStatusCode() != StatusCode.GOOD){ + resultCode = PlcResponseCode.NOT_FOUND; + }else{ + stringItem = OpcuaTcpPlcConnection.encodeFieldItem(value); + + } + Map> fields = new HashMap<>(); + Pair newPair = new ImmutablePair<>(resultCode, stringItem); + fields.put(fieldName, newPair); + PlcSubscriptionEvent event = new DefaultPlcSubscriptionEvent(Instant.now(), fields); + plcSubscriptionEventConsumer.accept(event); + }); + } + +} diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/model/OpcuaDataTypes.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/model/OpcuaDataTypes.java new file mode 100644 index 00000000000..af951375b6e --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/model/OpcuaDataTypes.java @@ -0,0 +1,46 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.plc4x.java.opcua.protocol.model; +/** + * @author Matthias Milan Stlrljic + * Created by Matthias Milan Stlrljic on 10.05.2019 + */ +public enum OpcuaDataTypes { + BOOL, + SByte, + Byte, + INT16, + UINT16, + INT32, + UINT32, + INT64, + UINT64, + FLOAT, + DOUBLE, + STATUS_CODE, + STRING, + DATE_TIME, + GUID, + BYTE_STRING, + XML_ELEMENT, + NODE_ID, + EXPANDABLE_NODE_ID, + QUALIFIED_NAME + +} diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/model/OpcuaIdentifierType.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/model/OpcuaIdentifierType.java new file mode 100644 index 00000000000..3122deb11b7 --- /dev/null +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/model/OpcuaIdentifierType.java @@ -0,0 +1,49 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ +package org.apache.plc4x.java.opcua.protocol.model; + +/** + * @author Matthias Milan Stlrljic + * Created by Matthias Milan Stlrljic on 10.05.2019 + */ +public enum OpcuaIdentifierType { + STRING_IDENTIFIER("s"), + NUMBER_IDENTIFIER("i"), + GUID_IDENTIFIER("g"), + BINARY_IDENTIFIER("BINARY_IDENTIFIER"); + + private String text; + + OpcuaIdentifierType(String text) { + this.text = text; + } + + public String getText() { + return this.text; + } + + public static OpcuaIdentifierType fromString(String text) { + for (OpcuaIdentifierType type : OpcuaIdentifierType.values()) { + if (type.text.equalsIgnoreCase(text)) { + return type; + } + } + throw new IllegalArgumentException("No constant with text " + text + " found"); + } +} diff --git a/plc4j/drivers/opcua/src/main/resources/META-INF/services/org.apache.plc4x.java.spi.PlcDriver b/plc4j/drivers/opcua/src/main/resources/META-INF/services/org.apache.plc4x.java.spi.PlcDriver new file mode 100644 index 00000000000..fe96e3ab7cc --- /dev/null +++ b/plc4j/drivers/opcua/src/main/resources/META-INF/services/org.apache.plc4x.java.spi.PlcDriver @@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +org.apache.plc4x.java.opcua.OpcuaPlcDriver diff --git a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/ManualPLC4XOpcua.java b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/ManualPLC4XOpcua.java new file mode 100644 index 00000000000..7708a89e5cd --- /dev/null +++ b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/ManualPLC4XOpcua.java @@ -0,0 +1,145 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.plc4x.java.opcua; + +import org.apache.plc4x.java.PlcDriverManager; +import org.apache.plc4x.java.api.exceptions.PlcConnectionException; +import org.apache.plc4x.java.api.messages.*; +import org.apache.plc4x.java.api.model.PlcConsumerRegistration; +import org.apache.plc4x.java.api.model.PlcField; +import org.apache.plc4x.java.api.types.PlcSubscriptionType; +import org.apache.plc4x.java.base.messages.DefaultPlcSubscriptionRequest; +import org.apache.plc4x.java.base.model.SubscriptionPlcField; +import org.apache.plc4x.java.opcua.connection.OpcuaTcpPlcConnection; +import org.apache.plc4x.java.opcua.protocol.OpcuaField; +import org.apache.plc4x.java.opcua.protocol.OpcuaPlcFieldHandler; + +import java.math.BigInteger; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.function.Consumer; +/** + * This class serves only as a manual entry point for ad-hoc tests of the OPC UA PLC4J driver. + * + * + * The current version is tested against a public server, which is to be replaced later by a separate instance of the Milo framework. + * Afterwards the code represented here will be used as an example for the introduction page. + * + * TODO: replace current public server with local Milo instance + * + * @author Matthias Milan Stlrljic + * Created by Matthias Milan Stlrljic on 10.05.2019 + */ +public class ManualPLC4XOpcua { + private static final String BOOL_IDENTIFIER = "ns=2;i=10844"; + private static final String BYTE_STRING_IDENTIFIER = "ns=2;i=10858"; + private static final String BYTE_IDENTIFIER = "ns=2;i=10846"; + private static final String DOUBLE_IDENTIFIER = "ns=2;i=10854"; + private static final String FLOAT_IDENTIFIER = "ns=2;i=10853"; + private static final String INT16_IDENTIFIER = "ns=2;i=10847"; + private static final String INT32_IDENTIFIER = "ns=2;i=10849"; + private static final String INT64_IDENTIFIER = "ns=2;i=10851"; + private static final String INTEGER_IDENTIFIER = "ns=2;i=10869"; + private static final String SBYTE_IDENTIFIER = "ns=2;i=10845"; + private static final String STRING_IDENTIFIER = "ns=2;i=10855"; + private static final String UINT16_IDENTIFIER = "ns=2;i=10848"; + private static final String UINT32_IDENTIFIER = "ns=2;i=10850"; + private static final String UINT64_IDENTIFIER = "ns=2;i=10852"; + private static final String UINTEGER_IDENTIFIER = "ns=2;i=10870"; + private static final String DOES_NOT_EXIST_IDENTIFIER = "ns=2;i=12512623"; + + + public static void main(String args[]){ + + + OpcuaTcpPlcConnection opcuaConnection = null; + OpcuaPlcFieldHandler fieldH = new OpcuaPlcFieldHandler(); + PlcField field = fieldH.createField("ns=2;i=10855"); + try { + opcuaConnection = (OpcuaTcpPlcConnection) + new PlcDriverManager().getConnection("opcua:tcp://opcua.demo-this.com:51210/UA/SampleServer"); + + } catch (PlcConnectionException e) { + e.printStackTrace(); + } + try { + PlcReadRequest.Builder builder = opcuaConnection.readRequestBuilder(); + builder.addItem("Bool", BOOL_IDENTIFIER); + builder.addItem("ByteString", BYTE_STRING_IDENTIFIER); + builder.addItem("Byte", BYTE_IDENTIFIER); + builder.addItem("Double", DOUBLE_IDENTIFIER); + builder.addItem("Float", FLOAT_IDENTIFIER); + builder.addItem("Int16", INT16_IDENTIFIER); + builder.addItem("Int32", INT32_IDENTIFIER); + builder.addItem("Int64", INT64_IDENTIFIER); + builder.addItem("Integer", INTEGER_IDENTIFIER); + builder.addItem("SByte", SBYTE_IDENTIFIER); + builder.addItem("String", STRING_IDENTIFIER); + builder.addItem("UInt16", UINT16_IDENTIFIER); + builder.addItem("UInt32", UINT32_IDENTIFIER); + builder.addItem("UInt64", UINT64_IDENTIFIER); + builder.addItem("UInteger", UINTEGER_IDENTIFIER); + + builder.addItem("DoesNotExists", DOES_NOT_EXIST_IDENTIFIER); + + PlcReadRequest request = builder.build(); + PlcReadResponse response = opcuaConnection.read(request).get(); + Collection coll = response.getAllStrings("String"); + + PlcWriteRequest.Builder wBuilder = opcuaConnection.writeRequestBuilder(); + wBuilder.addItem("w-Bool", BOOL_IDENTIFIER, true); + wBuilder.addItem("w-ByteString", BYTE_STRING_IDENTIFIER, "TEST".getBytes()); + wBuilder.addItem("w-Byte", BYTE_IDENTIFIER, (byte)1); + wBuilder.addItem("w-Double", DOUBLE_IDENTIFIER, (double)0.25); + wBuilder.addItem("w-Float", FLOAT_IDENTIFIER, (float)0.25); + wBuilder.addItem("w-INT16", INT16_IDENTIFIER, (short)12); + wBuilder.addItem("w-Int32", INT32_IDENTIFIER, (int)314); + wBuilder.addItem("w-Int64", INT64_IDENTIFIER, (long)123125); + wBuilder.addItem("w-Integer", INTEGER_IDENTIFIER, (int)314); + wBuilder.addItem("w-SByte", SBYTE_IDENTIFIER, (short)23); + wBuilder.addItem("w-String", STRING_IDENTIFIER, "TEST"); + wBuilder.addItem("w-UInt16", UINT16_IDENTIFIER, (int)222); + wBuilder.addItem("w-UInt32", UINT32_IDENTIFIER, (long)21412); + wBuilder.addItem("w-UInt64", UINT64_IDENTIFIER, new BigInteger("1245152")); + wBuilder.addItem("w-UInteger", UINTEGER_IDENTIFIER, new BigInteger("1245152")); + PlcWriteRequest writeRequest = wBuilder.build(); + PlcWriteResponse wResponse = opcuaConnection.write(writeRequest).get(); + + PlcSubscriptionResponse subResp = opcuaConnection.subscribe(new DefaultPlcSubscriptionRequest( + opcuaConnection, + new LinkedHashMap<>( + Collections.singletonMap("field1", + new SubscriptionPlcField(PlcSubscriptionType.CHANGE_OF_STATE, OpcuaField.of(STRING_IDENTIFIER), Duration.of(1, ChronoUnit.SECONDS))) + ) + )).get(); + + Consumer consumer = plcSubscriptionEvent -> System.out.println(plcSubscriptionEvent.toString()); + PlcConsumerRegistration registration = opcuaConnection.register(consumer, subResp.getSubscriptionHandles()); + Thread.sleep(7000); + registration.unregister(); + Thread.sleep(20000); + opcuaConnection.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaPlcDriverTest.java b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaPlcDriverTest.java new file mode 100644 index 00000000000..8c502ed2fed --- /dev/null +++ b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaPlcDriverTest.java @@ -0,0 +1,67 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.plc4x.java.opcua; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.apache.plc4x.java.opcua.OpcuaPlcDriver.INET_ADDRESS_PATTERN; +import static org.apache.plc4x.java.opcua.OpcuaPlcDriver.OPCUA_URI_PATTERN; +import static org.apache.plc4x.java.opcua.UtilsTest.assertMatching; +/** + * @author Matthias Milan Stlrljic + * Created by Matthias Milan Stlrljic on 10.05.2019 + */ +public class OpcuaPlcDriverTest { + @BeforeEach + public void before() { + } + + @AfterEach + public void after() { + + } + + @Test + public void testOpcuaAddressPattern() { + + assertMatching(INET_ADDRESS_PATTERN, "tcp://localhost"); + assertMatching(INET_ADDRESS_PATTERN, "tcp://localhost:3131"); + assertMatching(INET_ADDRESS_PATTERN, "tcp://www.google.de"); + assertMatching(INET_ADDRESS_PATTERN, "tcp://www.google.de:443"); + assertMatching(INET_ADDRESS_PATTERN, "tcp://127.0.0.1"); + assertMatching(INET_ADDRESS_PATTERN, "tcp://127.0.0.1:251"); + assertMatching(INET_ADDRESS_PATTERN, "tcp://254.254.254.254:1337"); + assertMatching(INET_ADDRESS_PATTERN, "tcp://254.254.254.254"); + + + assertMatching(OPCUA_URI_PATTERN, "opcua:tcp://localhost"); + assertMatching(OPCUA_URI_PATTERN, "opcua:tcp://localhost:3131"); + assertMatching(OPCUA_URI_PATTERN, "opcua:tcp://www.google.de"); + assertMatching(OPCUA_URI_PATTERN, "opcua:tcp://www.google.de:443"); + assertMatching(OPCUA_URI_PATTERN, "opcua:tcp://127.0.0.1"); + assertMatching(OPCUA_URI_PATTERN, "opcua:tcp://127.0.0.1:251"); + assertMatching(OPCUA_URI_PATTERN, "opcua:tcp://254.254.254.254:1337"); + assertMatching(OPCUA_URI_PATTERN, "opcua:tcp://254.254.254.254"); + + + } + +} diff --git a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/UtilsTest.java b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/UtilsTest.java new file mode 100644 index 00000000000..2e84a0b63a6 --- /dev/null +++ b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/UtilsTest.java @@ -0,0 +1,40 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.plc4x.java.opcua; + +import java.util.regex.Pattern; + +import static org.junit.Assert.fail; +/** + * @author Matthias Milan Stlrljic + * Created by Matthias Milan Stlrljic on 10.05.2019 + */ +public class UtilsTest { + public static void assertMatching(Pattern pattern, String match) { + if (!pattern.matcher(match).matches()) { + fail(pattern + "doesn't match " + match); + } + } + + public static void assertNoMatching(Pattern pattern, String match) { + if (pattern.matcher(match).matches()) { + fail(pattern + "does match " + match + " but should not"); + } + } +} diff --git a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/connection/OpcuaTcpPlcConnectionTest.java b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/connection/OpcuaTcpPlcConnectionTest.java new file mode 100644 index 00000000000..5a42c5e28ae --- /dev/null +++ b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/connection/OpcuaTcpPlcConnectionTest.java @@ -0,0 +1,36 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.plc4x.java.opcua.connection; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +/** + * @author Matthias Milan Stlrljic + * Created by Matthias Milan Stlrljic on 10.05.2019 + */ +public class OpcuaTcpPlcConnectionTest { + @BeforeEach + public void before() { + } + + @AfterEach + public void after() { + + } +} diff --git a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/OpcuaFieldTest.java b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/OpcuaFieldTest.java new file mode 100644 index 00000000000..828a0e6ac80 --- /dev/null +++ b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/OpcuaFieldTest.java @@ -0,0 +1,51 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.plc4x.java.opcua.protocol; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.apache.plc4x.java.opcua.UtilsTest.assertMatching; +import static org.apache.plc4x.java.opcua.protocol.OpcuaField.ADDRESS_PATTERN; +/** + * @author Matthias Milan Stlrljic + * Created by Matthias Milan Stlrljic on 10.05.2019 + */ +public class OpcuaFieldTest { + + @BeforeEach + public void before() { + } + + @AfterEach + public void after() { + + } + + @Test + public void testOpcuaAddressPattern() { + + assertMatching(ADDRESS_PATTERN, "ns=2;i=10846"); + assertMatching(ADDRESS_PATTERN, "ns=2;s=test.variable.name.inspect"); + assertMatching(ADDRESS_PATTERN, "ns=2;g=09087e75-8e5e-499b-954f-f2a8624db28a"); + assertMatching(ADDRESS_PATTERN, "ns=2;b=asvaewavarahreb=="); + + } +} diff --git a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/OpcuaPlcFieldHandlerTest.java b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/OpcuaPlcFieldHandlerTest.java new file mode 100644 index 00000000000..044de340e3f --- /dev/null +++ b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/OpcuaPlcFieldHandlerTest.java @@ -0,0 +1,36 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.plc4x.java.opcua.protocol; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +/** + * @author Matthias Milan Stlrljic + * Created by Matthias Milan Stlrljic on 10.05.2019 + */ +public class OpcuaPlcFieldHandlerTest { + @BeforeEach + public void before() { + } + + @AfterEach + public void after() { + + } +} diff --git a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandleTest.java b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandleTest.java new file mode 100644 index 00000000000..153d4ca85dc --- /dev/null +++ b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandleTest.java @@ -0,0 +1,36 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.plc4x.java.opcua.protocol; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +/** + * @author Matthias Milan Stlrljic + * Created by Matthias Milan Stlrljic on 10.05.2019 + */ +public class OpcuaSubscriptionHandleTest { + @BeforeEach + public void before() { + } + + @AfterEach + public void after() { + + } +} diff --git a/plc4j/drivers/pom.xml b/plc4j/drivers/pom.xml index 34f60a25c27..cf47c0761ca 100644 --- a/plc4j/drivers/pom.xml +++ b/plc4j/drivers/pom.xml @@ -39,6 +39,7 @@ modbus s7 simulated + opcua diff --git a/plc4j/protocols/driver-bases/base/src/main/java/org/apache/plc4x/java/base/messages/DefaultPlcWriteRequest.java b/plc4j/protocols/driver-bases/base/src/main/java/org/apache/plc4x/java/base/messages/DefaultPlcWriteRequest.java index 67dc29f5d01..641bc7eb5f0 100644 --- a/plc4j/protocols/driver-bases/base/src/main/java/org/apache/plc4x/java/base/messages/DefaultPlcWriteRequest.java +++ b/plc4j/protocols/driver-bases/base/src/main/java/org/apache/plc4x/java/base/messages/DefaultPlcWriteRequest.java @@ -209,12 +209,12 @@ public Builder addItem(String name, String fieldQuery, LocalDateTime... values) @Override public Builder addItem(String name, String fieldQuery, byte[]... values) { - return addItem(name, fieldQuery, values, fieldHandler::encodeDateTime); + return addItem(name, fieldQuery, values, fieldHandler::encodeByteArray); } @Override public Builder addItem(String name, String fieldQuery, Byte[]... values) { - return addItem(name, fieldQuery, values, fieldHandler::encodeDateTime); + return addItem(name, fieldQuery, values, fieldHandler::encodeByteArray); } @Override