Feature/native opua client (#253)
* Native OPCUA changes
* Started to refactor the SecureChannel to allow for large packets
* Still need to test it, but chunk continuation is supported.
* Chunk continuation is working now.
Still need to modify sending packets which are split into chunks
* Fix for policy id identifier and GUID Nodes
* Add plcConnection.close to read hello world example.
* Minor formatting changes, log removal, etc..
diff --git a/code-generation/language-base-freemarker/src/main/java/org/apache/plc4x/plugins/codegenerator/protocol/freemarker/BaseFreemarkerLanguageTemplateHelper.java b/code-generation/language-base-freemarker/src/main/java/org/apache/plc4x/plugins/codegenerator/protocol/freemarker/BaseFreemarkerLanguageTemplateHelper.java
index 11baf96..cbd292d 100644
--- a/code-generation/language-base-freemarker/src/main/java/org/apache/plc4x/plugins/codegenerator/protocol/freemarker/BaseFreemarkerLanguageTemplateHelper.java
+++ b/code-generation/language-base-freemarker/src/main/java/org/apache/plc4x/plugins/codegenerator/protocol/freemarker/BaseFreemarkerLanguageTemplateHelper.java
@@ -28,6 +28,7 @@
import org.apache.plc4x.plugins.codegenerator.types.references.StringTypeReference;
import org.apache.plc4x.plugins.codegenerator.types.references.TypeReference;
import org.apache.plc4x.plugins.codegenerator.types.terms.*;
+import org.w3c.dom.Node;
import java.util.*;
import java.util.stream.Collectors;
@@ -676,8 +677,10 @@
}
final SwitchField switchField = getSwitchField(baseType);
List<String> discriminatorNames = new ArrayList<>();
- for (Term discriminatorExpression : switchField.getDiscriminatorExpressions()) {
- discriminatorNames.add(getDiscriminatorName(discriminatorExpression));
+ if (switchField != null) {
+ for (Term discriminatorExpression : switchField.getDiscriminatorExpressions()) {
+ discriminatorNames.add(getDiscriminatorName(discriminatorExpression));
+ }
}
return discriminatorNames;
}
@@ -696,6 +699,23 @@
}
/**
+ * Check if there's any field with the given name.
+ * This is required to suppress the generation of a virtual field
+ * in case a discriminated field is providing the information.
+ *
+ * @param discriminatorName name of the virtual name
+ * @return true if a field with the given name already exists in the same type.
+ */
+ public boolean isDiscriminatorField(String discriminatorName) {
+ List<String> names = getDiscriminatorNames();
+ if (names != null) {
+ return getDiscriminatorNames().stream().anyMatch(
+ field -> field.equals(discriminatorName));
+ }
+ return false;
+ }
+
+ /**
* Converts a given discriminator description into a symbolic name.
*
* @param discriminatorExpression discriminator expression
diff --git a/code-generation/language-java/src/main/resources/templates/java/io-template.java.ftlh b/code-generation/language-java/src/main/resources/templates/java/io-template.java.ftlh
index 78f0251..34cfc7e 100644
--- a/code-generation/language-java/src/main/resources/templates/java/io-template.java.ftlh
+++ b/code-generation/language-java/src/main/resources/templates/java/io-template.java.ftlh
@@ -193,7 +193,28 @@
for(int curItem = 0; curItem < itemCount; curItem++) {
<#-- When parsing simple types, there is nothing that could require the "lastItem" -->
<#if !helper.isSimpleTypeReference(arrayField.type)>boolean lastItem = curItem == (itemCount - 1);</#if>
- ${arrayField.name}[curItem] = <#if helper.isSimpleTypeReference(arrayField.type)><#assign simpleTypeReference = arrayField.type>${helper.getReadBufferReadMethodCall("", simpleTypeReference, "", arrayField)}<#else>${arrayField.type.name}IO.staticParse(readBuffer<#if field.params?has_content>, <#list field.params as parserArgument>(${helper.getLanguageTypeNameForTypeReference(helper.getArgumentType(arrayField.type, parserArgument?index), true)}) (${helper.toParseExpression(arrayField, parserArgument, type.parserArguments)})<#sep>, </#sep></#list></#if>)<#if helper.isDiscriminatedChildTypeDefinition(helper.getTypeDefinitionForTypeReference(arrayField.type))>.build()</#if></#if>;
+ <@compress single_line=true>
+ ${arrayField.name}[curItem] =
+ <#if helper.isSimpleTypeReference(arrayField.type)>
+ <#assign simpleTypeReference = arrayField.type>
+ ${helper.getReadBufferReadMethodCall("", simpleTypeReference, "", arrayField)}
+ <#else>
+ ${arrayField.type.name}IO.staticParse(readBuffer
+ <#if field.params?has_content>
+ ,
+ <#list field.params as parserArgument>
+ <#if helper.getLanguageTypeNameForTypeReference(helper.getArgumentType(arrayField.type, parserArgument?index), true) = 'String'>
+ ${helper.getLanguageTypeNameForTypeReference(helper.getArgumentType(arrayField.type, parserArgument?index), true)}.valueOf(${helper.toParseExpression(arrayField, parserArgument, type.parserArguments)})<#sep>, </#sep>
+ <#else>
+ (${helper.getLanguageTypeNameForTypeReference(helper.getArgumentType(arrayField.type, parserArgument?index), true)}) (${helper.toParseExpression(arrayField, parserArgument, type.parserArguments)})<#sep>, </#sep>
+ </#if>
+ </#list>
+ </#if>)
+ <#if helper.isDiscriminatedChildTypeDefinition(helper.getTypeDefinitionForTypeReference(arrayField.type))>
+ .build()
+ </#if>
+ </#if>;
+ </@compress>
}
}
<#-- In all other cases do we have to work with a list, that is later converted to an array -->
@@ -267,7 +288,6 @@
<#break>
<#case "discriminator">
<#assign discriminatorField = field>
- <#assign simpleTypeReference = discriminatorField.type>
// Discriminator Field (${discriminatorField.name}) (Used as input to a switch field)
<#if helper.isEnumField(field)>
@@ -419,7 +439,26 @@
${helper.getLanguageTypeNameForField(simpleField)} ${simpleField.name} = ${helper.getLanguageTypeNameForField(simpleField)}.enumForValue(${helper.getReadBufferReadMethodCall(simpleField.type.name, helper.getEnumBaseTypeReference(simpleField.type), "", simpleField)});
<#else>
<#assign simpleFieldLogicalName><#if helper.isSimpleTypeReference(simpleField.type)>${simpleField.name}<#else>${simpleField.typeName}</#if></#assign>
- ${helper.getLanguageTypeNameForField(simpleField)} ${simpleField.name} = <#if helper.isSimpleTypeReference(simpleField.type)><#assign simpleTypeReference = simpleField.type>${helper.getReadBufferReadMethodCall(simpleFieldLogicalName, simpleTypeReference, "", simpleField)}<#else><#assign complexTypeReference = simpleField.type>${complexTypeReference.name}IO.staticParse(readBuffer<#if field.params?has_content>, <#list field.params as parserArgument>(${helper.getLanguageTypeNameForTypeReference(helper.getArgumentType(simpleField.type, parserArgument?index), true)}) (${helper.toParseExpression(simpleField, parserArgument, type.parserArguments)})<#sep>, </#sep></#list></#if>)</#if>;
+ <@compress single_line=true>
+ ${helper.getLanguageTypeNameForField(simpleField)} ${simpleField.name} =
+ <#if helper.isSimpleTypeReference(simpleField.type)>
+ <#assign simpleTypeReference = simpleField.type>
+ ${helper.getReadBufferReadMethodCall(simpleFieldLogicalName, simpleTypeReference, "", simpleField)}
+ <#else>
+ <#assign complexTypeReference = simpleField.type>
+ ${complexTypeReference.name}IO.staticParse(readBuffer
+ <#if field.params?has_content>
+ ,
+ <#list field.params as parserArgument>
+ <#if helper.getLanguageTypeNameForTypeReference(helper.getArgumentType(simpleField.type, parserArgument?index), true) = 'String'>
+ ${helper.getLanguageTypeNameForTypeReference(helper.getArgumentType(simpleField.type, parserArgument?index), true)}.valueOf(${helper.toParseExpression(simpleField, parserArgument, type.parserArguments)})<#sep>, </#sep>
+ <#else>
+ (${helper.getLanguageTypeNameForTypeReference(helper.getArgumentType(simpleField.type, parserArgument?index), true)}) (${helper.toParseExpression(simpleField, parserArgument, type.parserArguments)})<#sep>, </#sep>
+ </#if>
+ </#list>
+ </#if>)
+ </#if>;
+ </@compress>
</#if>
<#if !helper.isSimpleTypeReference(simpleField.type)>
readBuffer.closeContext("${simpleField.name}");
@@ -531,7 +570,6 @@
<#break>
<#case "discriminator">
<#assign discriminatorField = field>
- <#assign simpleTypeReference = discriminatorField.type>
// Discriminator Field (${discriminatorField.name}) (Used as input to a switch field)
${helper.getLanguageTypeNameForField(field)} ${discriminatorField.name} = (${helper.getLanguageTypeNameForField(field)}) _value.get${discriminatorField.name?cap_first}();
@@ -699,4 +737,4 @@
</#if>
}
-</#outputformat>
\ No newline at end of file
+</#outputformat>
diff --git a/code-generation/language-java/src/main/resources/templates/java/pojo-template.java.ftlh b/code-generation/language-java/src/main/resources/templates/java/pojo-template.java.ftlh
index db7083d..11d5dc4 100644
--- a/code-generation/language-java/src/main/resources/templates/java/pojo-template.java.ftlh
+++ b/code-generation/language-java/src/main/resources/templates/java/pojo-template.java.ftlh
@@ -157,6 +157,7 @@
</#list>
<#list type.virtualFields as field>
+ <#if !helper.isDiscriminatorField(field.name)>
public ${helper.getLanguageTypeNameForField(field)}<#if field.loopType??>[]</#if> get${field.name?cap_first}() {
<#if helper.getLanguageTypeNameForField(field) = 'String'>
return ${helper.getLanguageTypeNameForField(field)}.valueOf(${helper.toParseExpression(field, field.valueExpression, type.parserArguments)});
@@ -164,6 +165,7 @@
return (${helper.getLanguageTypeNameForField(field)}) (${helper.toParseExpression(field, field.valueExpression, type.parserArguments)});
</#if>
}
+ </#if>
</#list>
@Override
diff --git a/code-generation/protocol-base-mspec/src/main/java/org/apache/plc4x/plugins/codegenerator/language/mspec/model/definitions/DefaultComplexTypeDefinition.java b/code-generation/protocol-base-mspec/src/main/java/org/apache/plc4x/plugins/codegenerator/language/mspec/model/definitions/DefaultComplexTypeDefinition.java
index f8fb7c7..e3ca72f 100644
--- a/code-generation/protocol-base-mspec/src/main/java/org/apache/plc4x/plugins/codegenerator/language/mspec/model/definitions/DefaultComplexTypeDefinition.java
+++ b/code-generation/protocol-base-mspec/src/main/java/org/apache/plc4x/plugins/codegenerator/language/mspec/model/definitions/DefaultComplexTypeDefinition.java
@@ -71,6 +71,11 @@
field -> (AbstractField) field).collect(Collectors.toList());
}
+ public List<ImplicitField> getImplicitFields() {
+ return fields.stream().filter(field -> field instanceof ImplicitField).map(
+ field -> (ImplicitField) field).collect(Collectors.toList());
+ }
+
@Override
public List<VirtualField> getVirtualFields() {
return fields.stream().filter(field -> (field instanceof VirtualField)).map(field -> (VirtualField) field)
diff --git a/code-generation/protocol-test/src/main/resources/protocols/test/test.mspec b/code-generation/protocol-test/src/main/resources/protocols/test/test.mspec
index cac633f..8047343 100644
--- a/code-generation/protocol-test/src/main/resources/protocols/test/test.mspec
+++ b/code-generation/protocol-test/src/main/resources/protocols/test/test.mspec
@@ -98,12 +98,24 @@
]
[type 'AbstractTypeTest'
- [abstract bit 'bitField']
- [abstract int 8 'intField']
- [abstract uint 8 'uintField']
- [abstract float 8.23 'floatField']
- [abstract float 11.52 'doubleField']
- [abstract string '8' 'UTF-8' 'stringField']
+ //Abstract fields can only be used within discriminated base types.
+ [simple uint 8 'simpleField']
+ [abstract bit 'abstractBitField']
+ [abstract int 8 'abstractIntField']
+ [abstract uint 8 'abstractUintField']
+ [abstract float 8.23 'abstractFloatField']
+ [abstract float 11.52 'abstractDoubleField']
+ [abstract string '8' 'UTF-8' 'abstractStringField']
+ [typeSwitch 'simpleField'
+ ['0' AbstractedType
+ [simple bit 'abstractBitField']
+ [simple int 8 'abstractIntField']
+ [simple uint 8 'abstractUintField']
+ [simple float 8.23 'abstractFloatField']
+ [simple float 11.52 'abstractDoubleField']
+ [simple string '8' 'UTF-8' 'abstractStringField']
+ ]
+ ]
]
[type 'AbstractTypeTest'
@@ -333,6 +345,26 @@
]
]
+
+//Test to check if we can include concrete types as fields. Doesn't work in any language at the moment.
+//[discriminatedType 'SimpleDiscriminatedType'
+// [discriminator uint 8 'discr']
+// [typeSwitch 'discr'
+// ['0x00' SimpleDiscriminatedTypeA
+// [simple AnotherSimpleDiscriminatedTypeA 'simpA']
+// ]
+// ]
+//]
+
+//[discriminatedType 'AnotherSimpleDiscriminatedType'
+// [discriminator uint 8 'discr']
+// [typeSwitch 'discr'
+// ['0x00' AnotherSimpleDiscriminatedTypeA
+// [simple uint 8 'simpA']
+// ]
+// ]
+//]
+
////////////////////////////////////////////////////////////////
// Enumerated Type Tests
////////////////////////////////////////////////////////////////
diff --git a/plc4j/drivers/opcua/pom.xml b/plc4j/drivers/opcua/pom.xml
index 6f32daa..731e370 100644
--- a/plc4j/drivers/opcua/pom.xml
+++ b/plc4j/drivers/opcua/pom.xml
@@ -105,6 +105,8 @@
<usedDependencies combine.children="append">
<usedDependency>org.apache.plc4x:plc4x-code-generation-language-java</usedDependency>
<usedDependency>org.apache.plc4x:plc4x-protocols-opcua</usedDependency>
+ <usedDependency>org.bouncycastle:bcpkix-jdk15on</usedDependency>
+ <usedDependency>org.bouncycastle:bcprov-jdk15on</usedDependency>
</usedDependencies>
</configuration>
</plugin>
@@ -113,6 +115,17 @@
<dependencies>
<dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-buffer</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.plc4x</groupId>
+ <artifactId>plc4j-transport-tcp</artifactId>
+ <version>0.9.0-SNAPSHOT</version>
+ </dependency>
+
+ <dependency>
<groupId>org.apache.plc4x</groupId>
<artifactId>plc4j-api</artifactId>
<version>0.9.0-SNAPSHOT</version>
@@ -122,28 +135,31 @@
<artifactId>plc4j-spi</artifactId>
<version>0.9.0-SNAPSHOT</version>
</dependency>
+ <dependency>
+ <groupId>io.vavr</groupId>
+ <artifactId>vavr</artifactId>
+ </dependency>
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>sdk-client</artifactId>
+ <scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>stack-core</artifactId>
+ <scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>stack-client</artifactId>
+ <scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
- <dependency>
- <groupId>io.vavr</groupId>
- <artifactId>vavr</artifactId>
- </dependency>
<dependency>
<groupId>org.eclipse.milo</groupId>
@@ -152,29 +168,40 @@
</dependency>
<dependency>
- <groupId>org.apache.plc4x</groupId>
- <artifactId>plc4x-code-generation-language-java</artifactId>
- <version>0.9.0-SNAPSHOT</version>
- <!-- Scope is 'provided' as this way it's not shipped with the driver -->
- <scope>provided</scope>
- </dependency>
+ <groupId>org.apache.plc4x</groupId>
+ <artifactId>plc4x-code-generation-language-java</artifactId>
+ <version>0.9.0-SNAPSHOT</version>
+ <!-- Scope is 'provided' as this way it's not shipped with the driver -->
+ <scope>provided</scope>
+ </dependency>
- <dependency>
- <groupId>org.apache.plc4x</groupId>
- <artifactId>plc4x-protocols-opcua</artifactId>
- <version>0.9.0-SNAPSHOT</version>
- <!-- Scope is 'provided' as this way it's not shipped with the driver -->
- <scope>provided</scope>
- </dependency>
+ <dependency>
+ <groupId>org.apache.plc4x</groupId>
+ <artifactId>plc4x-protocols-opcua</artifactId>
+ <version>0.9.0-SNAPSHOT</version>
+ <!-- Scope is 'provided' as this way it's not shipped with the driver -->
+ <scope>provided</scope>
+ </dependency>
- <dependency>
- <groupId>org.apache.plc4x</groupId>
- <artifactId>plc4x-protocols-opcua</artifactId>
- <version>0.9.0-SNAPSHOT</version>
- <classifier>tests</classifier>
- <type>test-jar</type>
- <scope>test</scope>
- </dependency>
+ <dependency>
+ <groupId>org.apache.plc4x</groupId>
+ <artifactId>plc4x-protocols-opcua</artifactId>
+ <version>0.9.0-SNAPSHOT</version>
+ <classifier>tests</classifier>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcpkix-jdk15on</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk15on</artifactId>
+ <scope>compile</scope>
+ </dependency>
</dependencies>
<dependencyManagement>
@@ -183,17 +210,19 @@
<groupId>org.eclipse.milo</groupId>
<artifactId>sdk-client</artifactId>
<version>${milo.version}</version>
+ <scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>stack-core</artifactId>
<version>${milo.version}</version>
- <scope>provided</scope>
+ <scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>stack-client</artifactId>
<version>${milo.version}</version>
+ <scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.milo</groupId>
@@ -202,7 +231,6 @@
<scope>test</scope>
</dependency>
-
</dependencies>
</dependencyManagement>
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
index 4403142..1f55c85 100644
--- 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
@@ -18,35 +18,50 @@
*/
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.api.PlcDriver;
-import org.apache.plc4x.java.opcua.protocol.OpcuaField;
+import org.apache.plc4x.java.opcua.field.OpcuaField;
+import org.apache.plc4x.java.opcua.field.OpcuaPlcFieldHandler;
+import org.apache.plc4x.java.opcua.optimizer.OpcuaOptimizer;
+import org.apache.plc4x.java.opcua.protocol.*;
+import org.apache.plc4x.java.opcua.config.*;
+import org.apache.plc4x.java.opcua.readwrite.*;
+import org.apache.plc4x.java.opcua.readwrite.io.*;
+import org.apache.plc4x.java.spi.configuration.ConfigurationFactory;
+import org.apache.plc4x.java.spi.connection.*;
+import org.apache.plc4x.java.spi.transport.Transport;
+import org.apache.plc4x.java.spi.values.IEC61131ValueHandler;
+import org.apache.plc4x.java.api.value.PlcValueHandler;
+import org.apache.plc4x.java.spi.configuration.Configuration;
+import org.apache.plc4x.java.spi.connection.GeneratedDriverBase;
+import io.netty.buffer.ByteBuf;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
+import java.util.ServiceLoader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import java.util.function.ToIntFunction;
-/**
- * Implementation of the OPC UA protocol, based on:
- * - Eclipse Milo (https://github.com/eclipse/milo)
- *
- * Created by Matthias Milan Strljic on 10.05.2019
- */
-public class OpcuaPlcDriver implements PlcDriver {
+import static org.apache.plc4x.java.spi.configuration.ConfigurationFactory.configure;
+public class OpcuaPlcDriver extends GeneratedDriverBase<OpcuaAPU> {
- public static final Pattern INET_ADDRESS_PATTERN = Pattern.compile("(:(?<transport>tcp))?://(?<host>[\\w.-]+)(:(?<port>\\d*))?");
- public static final Pattern OPCUA_URI_PARAM_PATTERN = Pattern.compile("(?<param>[(\\?|\\&)([^=]+)\\=([^&]+)]+)?"); //later used for regex filtering of the params
- public static final Pattern OPCUA_URI_PATTERN = Pattern.compile("^opcua" + INET_ADDRESS_PATTERN + "(?<params>[\\w/=?&]+)?");
- private static final int requestTimeout = 10000;
- private OpcuaConnectionFactory opcuaConnectionFactory = new OpcuaConnectionFactory();
+ private static final Logger LOGGER = LoggerFactory.getLogger(OpcuaPlcDriver.class);
+ public static final Pattern INET_ADDRESS_PATTERN = Pattern.compile("(:(?<transportCode>tcp))?://" +
+ "(?<transportHost>[\\w.-]+)(:" +
+ "(?<transportPort>\\d*))?");
+
+ public static final Pattern URI_PATTERN = Pattern.compile("^(?<protocolCode>opcua)" +
+ INET_ADDRESS_PATTERN +
+ "(?<transportEndpoint>[\\w/=]*)[\\?]?" +
+ "(?<paramString>([^\\=]+\\=[^\\=&]+[&]?)*)"
+ );
+
+ private boolean isEncrypted;
@Override
public String getProtocolCode() {
@@ -55,33 +70,189 @@
@Override
public String getProtocolName() {
- return "OPC UA (TCP)";
+ return "Opcua";
}
@Override
- public PlcConnection getConnection(String url) throws PlcConnectionException {
- Matcher matcher = OPCUA_URI_PATTERN.matcher(url);
+ protected Class<? extends Configuration> getConfigurationType() {
+ return OpcuaConfiguration.class;
+ }
+ @Override
+ protected String getDefaultTransport() {
+ return "tcp";
+ }
+
+ @Override
+ protected boolean awaitSetupComplete() {
+ return true;
+ }
+
+ @Override
+ protected boolean awaitDiscoverComplete() {
+ return isEncrypted;
+ }
+
+ @Override
+ protected boolean canRead() {
+ return true;
+ }
+
+ @Override
+ protected boolean canWrite() {
+ return true;
+ }
+
+ @Override
+ protected boolean canSubscribe() {
+ return true;
+ }
+
+ @Override
+ protected OpcuaOptimizer getOptimizer() {
+ return new OpcuaOptimizer();
+ }
+
+ @Override
+ protected OpcuaPlcFieldHandler getFieldHandler() {
+ return new OpcuaPlcFieldHandler();
+ }
+
+ @Override
+ protected PlcValueHandler getValueHandler() {
+ return new IEC61131ValueHandler();
+ }
+
+ protected boolean awaitDisconnectComplete() {
+ return true;
+ }
+
+ @Override
+ protected ProtocolStackConfigurer<OpcuaAPU> getStackConfigurer() {
+ return SingleProtocolStackConfigurer.builder(OpcuaAPU.class, OpcuaAPUIO.class)
+ .withProtocol(OpcuaProtocolLogic.class)
+ .withPacketSizeEstimator(ByteLengthEstimator.class)
+ .withParserArgs(true)
+ .littleEndian()
+ .build();
+ }
+
+ @Override
+ public PlcConnection getConnection(String connectionString) throws PlcConnectionException {
+ // Split up the connection string into it's individual segments.
+ Matcher matcher = URI_PATTERN.matcher(connectionString);
if (!matcher.matches()) {
throw new PlcConnectionException(
- "Connection url doesn't match the format 'opcua:{type}//{host|port}'");
+ "Connection string doesn't match the format '{protocol-code}:({transport-code})?//{transport-host}(:{transport-port})(/{transport-endpoint})(?{parameter-string)?'");
+ }
+ final String protocolCode = matcher.group("protocolCode");
+ final String transportCode = (matcher.group("transportCode") != null) ?
+ matcher.group("transportCode") : getDefaultTransport();
+ final String transportHost = matcher.group("transportHost");
+ final String transportPort = matcher.group("transportPort");
+ final String transportEndpoint = matcher.group("transportEndpoint");
+ final String paramString = matcher.group("paramString");
+
+ // Check if the protocol code matches this driver.
+ if(!protocolCode.equals(getProtocolCode())) {
+ // Actually this shouldn't happen as the DriverManager should have not used this driver in the first place.
+ throw new PlcConnectionException(
+ "This driver is not suited to handle this connection string");
}
- 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) : "";
-
- try {
- return opcuaConnectionFactory.opcuaTcpPlcConnectionOf(InetAddress.getByName(host), port, params, requestTimeout);
- } catch (UnknownHostException e) {
- throw new PlcConnectionException(e);
+ // Create the configuration object.
+ OpcuaConfiguration configuration = (OpcuaConfiguration) new ConfigurationFactory().createConfiguration(
+ getConfigurationType(), paramString);
+ if(configuration == null) {
+ throw new PlcConnectionException("Unsupported configuration");
}
+ configuration.setTransportCode(transportCode);
+ configuration.setHost(transportHost);
+ configuration.setPort(transportPort);
+ configuration.setEndpoint("opc." + transportCode + "://" + transportHost + ":" + transportPort + "" + transportEndpoint);
+
+ // Try to find a transport in order to create a communication channel.
+ Transport transport = null;
+ ServiceLoader<Transport> transportLoader = ServiceLoader.load(
+ Transport.class, Thread.currentThread().getContextClassLoader());
+ for (Transport curTransport : transportLoader) {
+ if(curTransport.getTransportCode().equals(transportCode)) {
+ transport = curTransport;
+ break;
+ }
+ }
+ if(transport == null) {
+ throw new PlcConnectionException("Unsupported transport " + transportCode);
+ }
+
+ // Inject the configuration into the transport.
+ configure(configuration, transport);
+
+ // Create an instance of the communication channel which the driver should use.
+ ChannelFactory channelFactory = transport.createChannelFactory(transportHost + ":" + transportPort);
+ if(channelFactory == null) {
+ throw new PlcConnectionException("Unable to get channel factory from url " + transportHost + ":" + transportPort);
+ }
+ configure(configuration, channelFactory);
+
+ // Give drivers the option to customize the channel.
+ initializePipeline(channelFactory);
+
+ // Make the "await setup complete" overridable via system property.
+ boolean awaitSetupComplete = awaitSetupComplete();
+ if(System.getProperty(PROPERTY_PLC4X_FORCE_AWAIT_SETUP_COMPLETE) != null) {
+ awaitSetupComplete = Boolean.parseBoolean(System.getProperty(PROPERTY_PLC4X_FORCE_AWAIT_SETUP_COMPLETE));
+ }
+
+ // Make the "await disconnect complete" overridable via system property.
+ boolean awaitDisconnectComplete = awaitDisconnectComplete();
+ if(System.getProperty(PROPERTY_PLC4X_FORCE_AWAIT_DISCONNECT_COMPLETE) != null) {
+ awaitDisconnectComplete = Boolean.parseBoolean(System.getProperty(PROPERTY_PLC4X_FORCE_AWAIT_DISCONNECT_COMPLETE));
+ }
+
+ if (configuration.getSecurityPolicy() != null && !(configuration.getSecurityPolicy().equals("None"))) {
+ try {
+ configuration.openKeyStore();
+ } catch (Exception e) {
+ throw new PlcConnectionException("Unable to open keystore, please confirm you have the correct permissions");
+ }
+ }
+
+ this.isEncrypted = configuration.isEncrypted();
+
+ // Make the "await disconnect complete" overridable via system property.
+ boolean awaitDiscoverComplete = awaitDiscoverComplete();
+ if(System.getProperty(PROPERTY_PLC4X_FORCE_AWAIT_DISCOVER_COMPLETE) != null) {
+ awaitDiscoverComplete = Boolean.parseBoolean(System.getProperty(PROPERTY_PLC4X_FORCE_AWAIT_DISCOVER_COMPLETE));
+ }
+
+ return new DefaultNettyPlcConnection(
+ canRead(), canWrite(), canSubscribe(),
+ getFieldHandler(),
+ getValueHandler(),
+ configuration,
+ channelFactory,
+ awaitSetupComplete,
+ awaitDisconnectComplete,
+ awaitDiscoverComplete,
+ getStackConfigurer(),
+ getOptimizer());
}
@Override
public PlcConnection getConnection(String url, PlcAuthentication authentication) throws PlcConnectionException {
- throw new PlcConnectionException("opcua does not support Auth at this state");
+ throw new PlcConnectionException("Authentication not supported.");
+ }
+
+ /** Estimate the Length of a Packet */
+ public static class ByteLengthEstimator implements ToIntFunction<ByteBuf> {
+ @Override
+ public int applyAsInt(ByteBuf byteBuf) {
+ if (byteBuf.readableBytes() >= 8) {
+ return Integer.reverseBytes(byteBuf.getInt(byteBuf.readerIndex() + 4));
+ }
+ return -1;
+ }
}
@Override
diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/OpcuaConfiguration.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/OpcuaConfiguration.java
new file mode 100644
index 0000000..8857014
--- /dev/null
+++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/OpcuaConfiguration.java
@@ -0,0 +1,229 @@
+/*
+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.config;
+
+
+import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
+import org.apache.plc4x.java.opcua.context.CertificateGenerator;
+import org.apache.plc4x.java.opcua.context.CertificateKeyPair;
+import org.apache.plc4x.java.opcua.protocol.OpcuaProtocolLogic;
+import org.apache.plc4x.java.opcua.readwrite.PascalByteString;
+import org.apache.plc4x.java.spi.configuration.Configuration;
+import org.apache.plc4x.java.spi.configuration.annotations.ConfigurationParameter;
+import org.apache.plc4x.java.spi.configuration.annotations.defaults.BooleanDefaultValue;
+import org.apache.plc4x.java.spi.configuration.annotations.defaults.IntDefaultValue;
+import org.apache.plc4x.java.spi.configuration.annotations.defaults.StringDefaultValue;
+import org.apache.plc4x.java.transport.tcp.TcpTransportConfiguration;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.security.*;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+public class OpcuaConfiguration implements Configuration, TcpTransportConfiguration {
+
+ static {
+ // Required for SecurityPolicy.Aes256_Sha256_RsaPss
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(OpcuaConfiguration.class);
+
+ private String code;
+ private String host;
+ private String port;
+ private String endpoint;
+ private String params;
+ private Boolean isEncrypted = false;
+ private PascalByteString thumbprint;
+ private byte[] senderCertificate;
+
+ @ConfigurationParameter("discovery")
+ @BooleanDefaultValue(true)
+ private boolean discovery;
+
+ @ConfigurationParameter("username")
+ private String username;
+
+ @ConfigurationParameter("password")
+ private String password;
+
+ @ConfigurationParameter("securityPolicy")
+ @StringDefaultValue("None")
+ private String securityPolicy;
+
+ @ConfigurationParameter("keyStoreFile")
+ private String keyStoreFile;
+
+ @ConfigurationParameter("certDirectory")
+ private String certDirectory;
+
+ @ConfigurationParameter("keyStorePassword")
+ private String keyStorePassword;
+
+ private CertificateKeyPair ckp;
+
+ public boolean isDiscovery() {
+ return discovery;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public String getCertDirectory() {
+ return certDirectory;
+ }
+
+ public String getSecurityPolicy() {
+ return securityPolicy;
+ }
+
+ public String getKeyStoreFile() {
+ return keyStoreFile;
+ }
+
+ public String getKeyStorePassword() {
+ return keyStorePassword;
+ }
+
+ public PascalByteString getThumbprint() {
+ return thumbprint;
+ }
+
+ public CertificateKeyPair getCertificateKeyPair() {
+ return ckp;
+ }
+
+ public boolean isEncrypted() { return isEncrypted; }
+
+ public void setDiscovery(boolean discovery) {
+ this.discovery = discovery;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public void setCertDirectory(String certDirectory) {
+ this.certDirectory = certDirectory;
+ }
+
+ public void setSecurityPolicy(String securityPolicy) {
+ this.securityPolicy = securityPolicy;
+ }
+
+ public void setKeyStoreFile(String keyStoreFile) {
+ this.keyStoreFile = keyStoreFile;
+ }
+
+ public void setKeyStorePassword(String keyStorePassword) {
+ this.keyStorePassword = keyStorePassword;
+ }
+
+ public void setThumbprint(PascalByteString thumbprint) { this.thumbprint = thumbprint; }
+
+ public String getTransportCode() {
+ return code;
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public String getPort() {
+ return port;
+ }
+
+ public String getEndpoint() {
+ return endpoint;
+ }
+
+ public byte[] getSenderCertificate() {
+ return this.senderCertificate;
+ }
+
+ public void setTransportCode(String code) {
+ this.code = code;
+ }
+
+ public void setHost(String host) {
+ this.host = host;
+ }
+
+ public void setPort(String port) {
+ this.port = port;
+ }
+
+ public void setEndpoint(String endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ public void openKeyStore() throws Exception {
+ this.isEncrypted = true;
+ File securityTempDir = new File(certDirectory, "security");
+ if (!securityTempDir.exists() && !securityTempDir.mkdirs()) {
+ throw new PlcConnectionException("Unable to create directory please confirm folder permissions on " + certDirectory);
+ }
+ KeyStore keyStore = KeyStore.getInstance("PKCS12");
+ File serverKeyStore = securityTempDir.toPath().resolve(keyStoreFile).toFile();
+
+ File pkiDir = FileSystems.getDefault().getPath(certDirectory).resolve("pki").toFile();
+ if (!serverKeyStore.exists()) {
+ ckp = CertificateGenerator.generateCertificate();
+ LOGGER.info("Creating new KeyStore at {}", serverKeyStore);
+ keyStore.load(null, keyStorePassword.toCharArray());
+ keyStore.setKeyEntry("plc4x-certificate-alias", ckp.getKeyPair().getPrivate(), keyStorePassword.toCharArray(), new X509Certificate[] { ckp.getCertificate() });
+ keyStore.store(new FileOutputStream(serverKeyStore), keyStorePassword.toCharArray());
+ } else {
+ LOGGER.info("Loading KeyStore at {}", serverKeyStore);
+ keyStore.load(new FileInputStream(serverKeyStore), keyStorePassword.toCharArray());
+ String alias = keyStore.aliases().nextElement();
+ KeyPair kp = new KeyPair(keyStore.getCertificate(alias).getPublicKey(),
+ (PrivateKey) keyStore.getKey(alias, keyStorePassword.toCharArray()));
+ ckp = new CertificateKeyPair(kp,(X509Certificate) keyStore.getCertificate(alias));
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Configuration{" +
+ '}';
+ }
+
+ public void setSenderCertificate(byte[] certificate) { this.senderCertificate = certificate; }
+
+}
+
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
deleted file mode 100644
index feedc43..0000000
--- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/BaseOpcuaPlcConnection.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- 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.opcua.protocol.OpcuaPlcFieldHandler;
-import org.apache.plc4x.java.spi.connection.AbstractPlcConnection;
-import org.apache.plc4x.java.spi.messages.*;
-import org.apache.plc4x.java.spi.values.IEC61131ValueHandler;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- */
-public abstract class BaseOpcuaPlcConnection extends AbstractPlcConnection implements PlcReader, PlcWriter, PlcSubscriber {
-
- private static final Logger logger = LoggerFactory.getLogger(BaseOpcuaPlcConnection.class);
- protected boolean skipDiscovery = false;
-
- /**
- * @param params
- */
- 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) {
- case "discovery":
- skipDiscovery = !Boolean.valueOf(paramValue);
- break;
- 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(), new IEC61131ValueHandler());
- }
-
- @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);
- }
-
- public boolean isSkipDiscovery() {
- return skipDiscovery;
- }
-}
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
deleted file mode 100644
index d575bde..0000000
--- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/OpcuaConnectionFactory.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- 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;
-
-/**
- */
-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
deleted file mode 100644
index 458d5f1..0000000
--- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/connection/OpcuaTcpPlcConnection.java
+++ /dev/null
@@ -1,919 +0,0 @@
-/*
- 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.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.PlcSubscriptionField;
-import org.apache.plc4x.java.api.model.PlcSubscriptionHandle;
-import org.apache.plc4x.java.api.types.PlcResponseCode;
-import org.apache.plc4x.java.api.value.*;
-import org.apache.plc4x.java.opcua.protocol.OpcuaField;
-import org.apache.plc4x.java.opcua.protocol.OpcuaSubsriptionHandle;
-import org.apache.plc4x.java.spi.messages.*;
-import org.apache.plc4x.java.spi.messages.utils.ResponseItem;
-import org.apache.plc4x.java.spi.model.DefaultPlcConsumerRegistration;
-import org.apache.plc4x.java.spi.model.DefaultPlcSubscriptionField;
-import org.apache.plc4x.java.spi.values.*;
-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.StatusCodes;
-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.*;
-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.*;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.math.BigInteger;
-import java.math.BigDecimal;
-import java.util.stream.Collectors;
-import java.net.InetAddress;
-import java.time.Duration;
-import java.time.LocalDateTime;
-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 static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint;
-import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ulong;
-
-/**
- * Corresponding implementaion for a TCP-based connection for an OPC UA server.
- * TODO: At the moment are just connections without any security mechanism possible
- * <p>
- */
-public class OpcuaTcpPlcConnection extends BaseOpcuaPlcConnection {
-
- private static final int OPCUA_DEFAULT_TCP_PORT = 4840;
-
- private static final Logger logger = LoggerFactory.getLogger(OpcuaTcpPlcConnection.class);
- private final AtomicLong clientHandles = new AtomicLong(1L);
- private InetAddress address;
- private int requestTimeout = 5000;
- private int port;
- private String params;
- private OpcUaClient client;
- private boolean isConnected = false;
-
- 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());
- }
-
- private 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;
- }
-
- private OpcuaTcpPlcConnection(String params) {
- super(getOptionString(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 PlcValue encodePlcValue(DataValue value) {
- ExpandedNodeId typeNode = value.getValue().getDataType().get();
- Object objValue = value.getValue().getValue();
-
- if (objValue.getClass().isArray()) {
- Object[] objArray = (Object[]) objValue;
- if (objArray[0] instanceof Boolean) {
- Boolean[] obj = (Boolean[]) objValue;
- List<PlcValue> plcValue;
- {
- int itemCount = (int) obj.length;
- plcValue = new LinkedList<>();
-
- for(int curItem = 0; curItem < itemCount; curItem++) {
- plcValue.add(new PlcBOOL((Boolean) obj[curItem]));
- }
- }
- return new PlcList(plcValue);
- } else if (objArray[0] instanceof Integer) {
- Integer[] obj = (Integer[]) objValue;
- List<PlcValue> plcValue;
- {
- int itemCount = (int) obj.length;
- plcValue = new LinkedList<>();
-
- for(int curItem = 0; curItem < itemCount; curItem++) {
- plcValue.add(new PlcDINT((Integer) obj[curItem]));
- }
- }
- return new PlcList(plcValue);
- } else if (objArray[0] instanceof Short) {
- Short[] obj = (Short[]) objValue;
- List<PlcValue> plcValue;
- {
- int itemCount = (int) obj.length;
- plcValue = new LinkedList<>();
-
- for(int curItem = 0; curItem < itemCount; curItem++) {
- plcValue.add(new PlcINT((Short) obj[curItem]));
- }
- }
- return new PlcList(plcValue);
- } else if (objArray[0] instanceof Byte) {
- Byte[] obj = (Byte[]) objValue;
- List<PlcValue> plcValue;
- {
- int itemCount = (int) obj.length;
- plcValue = new LinkedList<>();
-
- for(int curItem = 0; curItem < itemCount; curItem++) {
- plcValue.add(new PlcSINT((Byte) obj[curItem]));
- }
- }
- return new PlcList(plcValue);
- } else if (objArray[0] instanceof Long) {
- Long[] obj = (Long[]) objValue;
- List<PlcValue> plcValue;
- {
- int itemCount = (int) obj.length;
- plcValue = new LinkedList<>();
-
- for(int curItem = 0; curItem < itemCount; curItem++) {
- plcValue.add(new PlcLINT((Long) obj[curItem]));
- }
- }
- return new PlcList(plcValue);
- } else if (objArray[0] instanceof Float) {
- Float[] obj = (Float[]) objValue;
- List<PlcValue> plcValue;
- {
- int itemCount = (int) obj.length;
- plcValue = new LinkedList<>();
-
- for(int curItem = 0; curItem < itemCount; curItem++) {
- plcValue.add(new PlcREAL((Float) obj[curItem]));
- }
- }
- return new PlcList(plcValue);
- } else if (objArray[0] instanceof Double) {
- Double[] obj = (Double[]) objValue;
- List<PlcValue> plcValue;
- {
- int itemCount = (int) obj.length;
- plcValue = new LinkedList<>();
-
- for(int curItem = 0; curItem < itemCount; curItem++) {
- plcValue.add(new PlcLREAL((Double) obj[curItem]));
- }
- }
- return new PlcList(plcValue);
- } else if (objArray[0] instanceof String) {
- String[] obj = (String[]) objValue;
- List<PlcValue> plcValue;
- {
- int itemCount = (int) obj.length;
- plcValue = new LinkedList<>();
-
- for(int curItem = 0; curItem < itemCount; curItem++) {
- plcValue.add(new PlcSTRING((String) obj[curItem]));
- }
- }
- return new PlcList(plcValue);
- } else {
- logger.warn("Node type for " + objArray[0].getClass() + " is not supported");
- return null;
- }
-
- } else {
- if (typeNode.equals(Identifiers.Boolean)) {
- return new PlcBOOL((Boolean) objValue);
- } else if (typeNode.equals(Identifiers.Integer)) {
- return new PlcDINT((Integer) objValue);
- } else if (typeNode.equals(Identifiers.Int16)) {
- return new PlcINT((Short) objValue);
- } else if (typeNode.equals(Identifiers.Int32)) {
- return new PlcDINT((Integer) objValue);
- } else if (typeNode.equals(Identifiers.Int64)) {
- return new PlcLINT((Long) objValue);
- } else if (typeNode.equals(Identifiers.UInteger)) {
- return new PlcLINT((Long) objValue);
- } else if (typeNode.equals(Identifiers.UInt16)) {
- return new PlcUINT(((UShort) objValue).intValue());
- } else if (typeNode.equals(Identifiers.UInt32)) {
- return new PlcUDINT(((UInteger) objValue).longValue());
- } else if (typeNode.equals(Identifiers.UInt64)) {
- return new PlcULINT(new BigInteger(objValue.toString()));
- } else if (typeNode.equals(Identifiers.Byte)) {
- return new PlcINT(Short.valueOf(objValue.toString()));
- } else if (typeNode.equals(Identifiers.Float)) {
- return new PlcREAL((Float) objValue);
- } else if (typeNode.equals(Identifiers.Double)) {
- return new PlcLREAL((Double) objValue);
- } else if (typeNode.equals(Identifiers.SByte)) {
- return new PlcSINT((Byte) objValue);
- } else {
- return new PlcSTRING(objValue.toString());
- }
- }
-
- }
-
- public InetAddress getRemoteAddress() {
- return address;
- }
-
- @Override
- public void connect() throws PlcConnectionException {
- List<EndpointDescription> endpoints = null;
- EndpointDescription endpoint = null;
-
- try {
- endpoints = DiscoveryClient.getEndpoints(getEndpointUrl(address, port, getSubPathOfParams(params))).get();
- //TODO Exception should be handeled better when the Discovery-API of Milo is stable
- } catch (Exception ex) {
- logger.info("Failed to discover Endpoint with enabled discovery. If the endpoint does not allow a correct discovery disable this option with the nDiscovery=true option. Failed Endpoint: {}", getEndpointUrl(address, port, params));
-
- // try the explicit discovery endpoint as well
- String discoveryUrl = getEndpointUrl(address, port, getSubPathOfParams(params));
-
- if (!discoveryUrl.endsWith("/")) {
- discoveryUrl += "/";
- }
- discoveryUrl += "discovery";
-
- logger.info("Trying explicit discovery URL: {}", discoveryUrl);
- try {
- endpoints = DiscoveryClient.getEndpoints(discoveryUrl).get();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new PlcConnectionException("Unable to discover URL:" + discoveryUrl);
- } catch (ExecutionException e) {
- throw new PlcConnectionException("Unable to discover URL:" + discoveryUrl);
- }
-
- }
- endpoint = endpoints.stream()
- .filter(e -> e.getSecurityPolicyUri().equals(getSecurityPolicy().getUri()))
- .filter(endpointFilter())
- .findFirst()
- .orElseThrow(() -> new PlcConnectionException("No desired endpoints from"));
-
- if (this.skipDiscovery) {
- //ApplicationDescription applicationDescription = new ApplicationDescription();
- //endpoint = new EndpointDescription(address.getHostAddress(),applicationDescription , null, MessageSecurityMode.None, SecurityPolicy.None.getUri(), null , TransportProfile.TCP_UASC_UABINARY.getUri(), UByte.valueOf(0));// TODO hier machen wenn fertig
- ApplicationDescription currentAD = endpoint.getServer();
- ApplicationDescription withoutDiscoveryAD = new ApplicationDescription(
- currentAD.getApplicationUri(),
- currentAD.getProductUri(),
- currentAD.getApplicationName(),
- currentAD.getApplicationType(),
- currentAD.getGatewayServerUri(),
- currentAD.getDiscoveryProfileUri(),
- new String[0]);
- //try to replace the overhanded address
- //any error will result in the overhanded address of the client
- String newEndpointUrl = endpoint.getEndpointUrl(), prefix = "", suffix = "";
- String splitterPrefix = "://";
- String splitterSuffix = ":";
- String[] prefixSplit = newEndpointUrl.split(splitterPrefix);
- if (prefixSplit.length > 1) {
- String[] suffixSplit = prefixSplit[1].split(splitterSuffix);
- //reconstruct the uri
- newEndpointUrl = "";
- newEndpointUrl += prefixSplit[0] + splitterPrefix + address.getHostAddress();
- for (int suffixCounter = 1; suffixCounter < suffixSplit.length; suffixCounter++) {
- newEndpointUrl += splitterSuffix + suffixSplit[suffixCounter];
- }
- // attach surounding prefix match
- for (int prefixCounter = 2; prefixCounter < prefixSplit.length; prefixCounter++) {
- newEndpointUrl += splitterPrefix + prefixSplit[prefixCounter];
- }
- }
-
- EndpointDescription noDiscoverEndpoint = new EndpointDescription(
- newEndpointUrl,
- withoutDiscoveryAD,
- endpoint.getServerCertificate(),
- endpoint.getSecurityMode(),
- endpoint.getSecurityPolicyUri(),
- endpoint.getUserIdentityTokens(),
- endpoint.getTransportProfileUri(),
- endpoint.getSecurityLevel());
- endpoint = noDiscoverEndpoint;
- }
-
-
- 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 e) {
- isConnected = false;
- Thread.currentThread().interrupt();
- throw new PlcConnectionException("Error while creation of the connection because of : " + e.getMessage());
- } catch (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<PlcSubscriptionResponse> subscribe(PlcSubscriptionRequest subscriptionRequest) {
- CompletableFuture<PlcSubscriptionResponse> future = CompletableFuture.supplyAsync(() -> {
- Map<String, ResponseItem<PlcSubscriptionHandle>> responseItems = new HashMap<>();
- for (String fieldName : subscriptionRequest.getFieldNames()) {
- final DefaultPlcSubscriptionField subscriptionField = (DefaultPlcSubscriptionField) subscriptionRequest.getField(fieldName);
- final OpcuaField field = (OpcuaField) Objects.requireNonNull(subscriptionField.getPlcField());
- long cycleTime = subscriptionField.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());
-
- MonitoringMode monitoringMode;
- switch (subscriptionField.getPlcSubscriptionType()) {
- case CYCLIC:
- monitoringMode = MonitoringMode.Sampling;
- break;
- case CHANGE_OF_STATE:
- monitoringMode = MonitoringMode.Reporting;
- cycleTime = subscriptionField.getDuration().orElse(Duration.ofSeconds(0)).toMillis();
- break;
- case EVENT:
- monitoringMode = MonitoringMode.Reporting;
- break;
- default:
- monitoringMode = MonitoringMode.Reporting;
- }
-
- MonitoringParameters parameters = new MonitoringParameters(
- clientHandle,
- (double) cycleTime, // sampling interval
- null, // filter, null means use default
- uint(1), // queue size
- true // discard oldest
- );
-
- PlcSubscriptionHandle subHandle = null;
- PlcResponseCode responseCode = PlcResponseCode.ACCESS_DENIED;
- try {
- UaSubscription subscription = client.getSubscriptionManager().createSubscription(cycleTime).get();
-
- MonitoredItemCreateRequest request = new MonitoredItemCreateRequest(
- readValueId, monitoringMode, parameters);
- List<MonitoredItemCreateRequest> requestList = new LinkedList<>();
- requestList.add(request);
- OpcuaSubsriptionHandle subsriptionHandle = new OpcuaSubsriptionHandle(fieldName, clientHandle);
- BiConsumer<UaMonitoredItem, Integer> onItemCreated =
- (item, id) -> item.setValueConsumer(subsriptionHandle::onSubscriptionValue);
-
- List<UaMonitoredItem> items = subscription.createMonitoredItems(
- TimestampsToReturn.Both,
- requestList,
- onItemCreated
- ).get();
-
- subHandle = subsriptionHandle;
- responseCode = PlcResponseCode.OK;
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- logger.warn("Unable to subscribe Elements because of: {}", e.getMessage());
- } catch (ExecutionException e) {
- logger.warn("Unable to subscribe Elements because of: {}", e.getMessage());
- }
-
- responseItems.put(fieldName, new ResponseItem(responseCode, subHandle));
- }
- return new DefaultPlcSubscriptionResponse(subscriptionRequest, responseItems);
- });
-
- return future;
- }
-
- @Override
- public CompletableFuture<PlcUnsubscriptionResponse> unsubscribe(PlcUnsubscriptionRequest unsubscriptionRequest) {
- unsubscriptionRequest.getSubscriptionHandles().forEach(o -> {
- OpcuaSubsriptionHandle opcSubHandle = (OpcuaSubsriptionHandle) o;
- try {
- client.getSubscriptionManager().deleteSubscription(opcSubHandle.getClientHandle()).get();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- logger.warn("Unable to unsubscribe Elements because of: {}", e.getMessage());
- } catch (ExecutionException e) {
- logger.warn("Unable to unsubscribe Elements because of: {}", e.getMessage());
- }
- });
-
- return null;
- }
-
- @Override
- public PlcConsumerRegistration register(Consumer<PlcSubscriptionEvent> consumer, Collection<PlcSubscriptionHandle> handles) {
- List<PlcConsumerRegistration> registrations = new LinkedList<>();
- // Register the current consumer for each of the given subscription handles
- for (PlcSubscriptionHandle subscriptionHandle : handles) {
- final PlcConsumerRegistration consumerRegistration = subscriptionHandle.register(consumer);
- registrations.add(consumerRegistration);
- }
-
- return new DefaultPlcConsumerRegistration(this, consumer, handles.toArray(new PlcSubscriptionHandle[0]));
- }
-
- @Override
- public void unregister(PlcConsumerRegistration registration) {
- registration.unregister();
- }
-
- @Override
- public CompletableFuture<PlcReadResponse> read(PlcReadRequest readRequest) {
- CompletableFuture<PlcReadResponse> future = CompletableFuture.supplyAsync(() -> {
- readRequest.getFields();
- Map<String, ResponseItem<PlcValue>> fields = new HashMap<>();
- List<NodeId> readValueIds = new LinkedList<>();
- List<PlcField> readPLCValues = readRequest.getFields();
- for (PlcField field : readPLCValues) {
- NodeId idNode = generateNodeId((OpcuaField) field);
- readValueIds.add(idNode);
- }
-
- CompletableFuture<List<DataValue>> dataValueCompletableFuture = client.readValues(0.0, TimestampsToReturn.Both, readValueIds);
- List<DataValue> readValues = null;
- try {
- readValues = dataValueCompletableFuture.get();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- logger.warn("Unable to read Elements because of: {}", e.getMessage());
- } catch (ExecutionException e) {
- logger.warn("Unable to read Elements because of: {}", e.getMessage());
- }
- for (int counter = 0; counter < readValueIds.size(); counter++) {
- PlcResponseCode resultCode = PlcResponseCode.OK;
- PlcValue stringItem = null;
- if (readValues == null || readValues.size() <= counter ||
- !readValues.get(counter).getStatusCode().equals(StatusCode.GOOD)) {
- resultCode = PlcResponseCode.NOT_FOUND;
- } else {
- stringItem = encodePlcValue(readValues.get(counter));
-
- }
- ResponseItem<PlcValue> newPair = new ResponseItem<>(resultCode, stringItem);
- fields.put((String) readRequest.getFieldNames().toArray()[counter], newPair);
-
-
- }
- return new DefaultPlcReadResponse(readRequest, fields);
- });
-
- return future;
- }
-
-
- @Override
- public CompletableFuture<PlcWriteResponse> write(PlcWriteRequest writeRequest) {
- CompletableFuture<PlcWriteResponse> future;
- future = CompletableFuture.supplyAsync(() -> {
- List<PlcField> writePLCValues = writeRequest.getFields();
- LinkedList<DataValue> values = new LinkedList<>();
- LinkedList<NodeId> ids = new LinkedList<>();
- LinkedList<String> names = new LinkedList<>();
- Map<String, PlcResponseCode> fieldResponse = new HashMap<>();
- for (String fieldName : writeRequest.getFieldNames()) {
- OpcuaField uaField = (OpcuaField) writeRequest.getField(fieldName);
- NodeId idNode = generateNodeId(uaField);
- Object valueObject = writeRequest.getPlcValue(fieldName).getObject();
- // Added small work around for handling BigIntegers as input type for UInt64
- if (valueObject instanceof BigInteger) valueObject = ulong((BigInteger) valueObject);
- Variant var = null;
- if (valueObject instanceof ArrayList) {
- List<PlcValue> plcValueList = (List<PlcValue>) valueObject;
- String dataType = uaField.getPlcDataType();
- if (dataType.equals("NULL")) {
- if (plcValueList.get(0).getObject() instanceof Boolean) {
- dataType = "BOOL";
- } else if (plcValueList.get(0).getObject() instanceof Byte) {
- dataType = "SINT";
- } else if (plcValueList.get(0).getObject() instanceof Short) {
- dataType = "INT";
- } else if (plcValueList.get(0).getObject() instanceof Integer) {
- dataType = "DINT";
- } else if (plcValueList.get(0).getObject() instanceof Long) {
- dataType = "LINT";
- } else if (plcValueList.get(0).getObject() instanceof Float) {
- dataType = "REAL";
- } else if (plcValueList.get(0).getObject() instanceof Double) {
- dataType = "LREAL";
- } else if (plcValueList.get(0).getObject() instanceof String) {
- dataType = "STRING";
- }
- }
- switch (dataType) {
- case "BOOL":
- case "BIT":
- List<Boolean> booleanList = (plcValueList).stream().map(
- x -> ((PlcBOOL) x).getBoolean()).collect(Collectors.toList());
- var = new Variant(booleanList.toArray(new Boolean[booleanList.size()]));
- break;
- case "BYTE":
- case "BITARR8":
- List<UByte> byteList = (plcValueList).stream().map(
- x -> UByte.valueOf(((PlcBYTE) x).getShort())).collect(Collectors.toList());
- var = new Variant(byteList.toArray(new UByte[byteList.size()]));
- break;
- case "SINT":
- case "INT8":
- List<Byte> sintList = (plcValueList).stream().map(
- x -> ((PlcSINT) x).getByte()).collect(Collectors.toList());
- var = new Variant(sintList.toArray(new Byte[sintList.size()]));
- break;
- case "USINT":
- case "UINT8":
- case "BIT8":
- List<UByte> usintList = (plcValueList).stream().map(
- x -> UByte.valueOf(((PlcUSINT) x).getShort())).collect(Collectors.toList());
- var = new Variant(usintList.toArray(new UByte[usintList.size()]));
- break;
- case "INT":
- case "INT16":
- List<Short> intList = (plcValueList).stream().map(
- x -> ((PlcINT) x).getShort()).collect(Collectors.toList());
- var = new Variant(intList.toArray(new Short[intList.size()]));
- break;
- case "UINT":
- case "UINT16":
- List<UShort> uintList = (plcValueList).stream().map(
- x -> UShort.valueOf(((PlcUINT) x).getInteger())).collect(Collectors.toList());
- var = new Variant(uintList.toArray(new UShort[uintList.size()]));
- break;
- case "WORD":
- case "BITARR16":
- List<UShort> wordList = (plcValueList).stream().map(
- x -> UShort.valueOf(((PlcWORD) x).getInteger())).collect(Collectors.toList());
- var = new Variant(wordList.toArray(new UShort[wordList.size()]));
- break;
- case "DINT":
- case "INT32":
- List<Integer> dintList = (plcValueList).stream().map(
- x -> ((PlcDINT) x).getInteger()).collect(Collectors.toList());
- var = new Variant(dintList.toArray(new Integer[dintList.size()]));
- break;
- case "UDINT":
- case "UINT32":
- List<UInteger> udintList = (plcValueList).stream().map(
- x -> UInteger.valueOf(((PlcUDINT) x).getLong())).collect(Collectors.toList());
- var = new Variant(udintList.toArray(new UInteger[udintList.size()]));
- break;
- case "DWORD":
- case "BITARR32":
- List<UInteger> dwordList = (plcValueList).stream().map(
- x -> UInteger.valueOf(((PlcDWORD) x).getLong())).collect(Collectors.toList());
- var = new Variant(dwordList.toArray(new UInteger[dwordList.size()]));
- break;
- case "LINT":
- case "INT64":
- List<Long> lintList = (plcValueList).stream().map(
- x -> ((PlcLINT) x).getLong()).collect(Collectors.toList());
- var = new Variant(lintList.toArray(new Long[lintList.size()]));
- break;
- case "ULINT":
- case "UINT64":
- List<ULong> ulintList = (plcValueList).stream().map(
- x -> ULong.valueOf(((PlcULINT) x).getBigInteger())).collect(Collectors.toList());
- var = new Variant(ulintList.toArray(new ULong[ulintList.size()]));
- break;
- case "LWORD":
- case "BITARR64":
- List<ULong> lwordList = (plcValueList).stream().map(
- x -> ULong.valueOf(((PlcLWORD) x).getBigInteger())).collect(Collectors.toList());
- var = new Variant(lwordList.toArray(new ULong[lwordList.size()]));
- break;
- case "REAL":
- case "FLOAT":
- List<Float> realList = (plcValueList).stream().map(
- x -> ((PlcREAL) x).getFloat()).collect(Collectors.toList());
- var = new Variant(realList.toArray(new Float[realList.size()]));
- break;
- case "LREAL":
- case "DOUBLE":
- List<Double> lrealList = (plcValueList).stream().map(
- x -> (Double) ((PlcLREAL) x).getDouble()).collect(Collectors.toList());
- var = new Variant(lrealList.toArray(new Double[lrealList.size()]));
- break;
- case "CHAR":
- List<String> charList = (plcValueList).stream().map(
- x -> ((PlcCHAR) x).getString()).collect(Collectors.toList());
- var = new Variant(charList.toArray(new String[charList.size()]));
- break;
- case "WCHAR":
- List<String> wcharList = (plcValueList).stream().map(
- x -> ((PlcWCHAR) x).getString()).collect(Collectors.toList());
- var = new Variant(wcharList.toArray(new String[wcharList.size()]));
- break;
- case "STRING":
- List<String> stringList = (plcValueList).stream().map(
- x -> ((PlcSTRING) x).getString()).collect(Collectors.toList());
- var = new Variant(stringList.toArray(new String[stringList.size()]));
- break;
- case "WSTRING":
- case "STRING16":
- List<String> wstringList = (plcValueList).stream().map(
- x -> (String) ((PlcSTRING) x).getString()).collect(Collectors.toList());
- var = new Variant(wstringList.toArray(new String[wstringList.size()]));
- break;
- case "DATE_AND_TIME":
- List<LocalDateTime> dateTimeList = (plcValueList).stream().map(
- x -> ((PlcDATE_AND_TIME) x).getDateTime()).collect(Collectors.toList());
- var = new Variant(dateTimeList.toArray(new LocalDateTime[dateTimeList.size()]));
- break;
- default:
- logger.warn("Unsupported data type : {}, {}", plcValueList.get(0).getClass(), dataType);
- }
- } else {
- String dataType = uaField.getPlcDataType();
- PlcValue plcValue = (PlcValue) writeRequest.getPlcValue(fieldName);
-
- if (dataType.equals("NULL")) {
- if (plcValue.getObject() instanceof Boolean) {
- dataType = "BOOL";
- } else if (plcValue.getObject() instanceof Byte) {
- dataType = "SINT";
- } else if (plcValue.getObject() instanceof Short) {
- dataType = "INT";
- } else if (plcValue.getObject() instanceof Integer) {
- dataType = "DINT";
- } else if (plcValue.getObject() instanceof Long) {
- dataType = "LINT";
- } else if (plcValue.getObject() instanceof Float) {
- dataType = "REAL";
- } else if (plcValue.getObject() instanceof Double) {
- dataType = "LREAL";
- } else if (plcValue.getObject() instanceof String) {
- dataType = "STRING";
- }
- }
- switch (dataType) {
- case "BOOL":
- case "BIT":
- var = new Variant(plcValue.getBoolean());
- break;
- case "BYTE":
- case "BITARR8":
- var = new Variant(UByte.valueOf(plcValue.getShort()));
- break;
- case "SINT":
- case "INT8":
- var = new Variant(plcValue.getByte());
- break;
- case "USINT":
- case "UINT8":
- case "BIT8":
- var = new Variant(UByte.valueOf(plcValue.getShort()));
- break;
- case "INT":
- case "INT16":
- var = new Variant(plcValue.getShort());
- break;
- case "UINT":
- case "UINT16":
- var = new Variant(UShort.valueOf(plcValue.getInteger()));
- break;
- case "WORD":
- case "BITARR16":
- var = new Variant(UShort.valueOf(plcValue.getInteger()));
- break;
- case "DINT":
- case "INT32":
- var = new Variant(plcValue.getInteger());
- break;
- case "UDINT":
- case "UINT32":
- var = new Variant(UInteger.valueOf(plcValue.getLong()));
- break;
- case "DWORD":
- case "BITARR32":
- var = new Variant(UInteger.valueOf(plcValue.getLong()));
- break;
- case "LINT":
- case "INT64":
- var = new Variant(plcValue.getLong());
- break;
- case "ULINT":
- case "UINT64":
- var = new Variant(ULong.valueOf(plcValue.getBigInteger()));
- break;
- case "LWORD":
- case "BITARR64":
- var = new Variant(ULong.valueOf(plcValue.getBigInteger()));
- break;
- case "REAL":
- case "FLOAT":
- var = new Variant(plcValue.getFloat());
- break;
- case "LREAL":
- case "DOUBLE":
- var = new Variant(plcValue.getDouble());
- break;
- case "CHAR":
- var = new Variant(plcValue.getString());
- break;
- case "WCHAR":
- var = new Variant(plcValue.getString());
- break;
- case "STRING":
- var = new Variant(plcValue.getString());
- break;
- case "WSTRING":
- case "STRING16":
- var = new Variant(plcValue.getString());
- break;
- case "DATE_AND_TIME":
- var = new Variant(plcValue.getDateTime());
- break;
- default:
- logger.warn("Unsupported data type : {}, {}", plcValue.getClass(), dataType);
- }
- }
- DataValue value = new DataValue(var, StatusCode.GOOD, null, null);
- ids.add(idNode);
- names.add(fieldName);
- values.add(value);
- }
- CompletableFuture<List<StatusCode>> opcRequest =
- client.writeValues(ids, values);
- List<StatusCode> statusCodes = null;
- try {
- statusCodes = opcRequest.get();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- statusCodes = new LinkedList<>();
- for (int counter = 0; counter < ids.size(); counter++) {
- ((LinkedList<StatusCode>) statusCodes).push(StatusCode.BAD);
- }
- } catch (ExecutionException e) {
- statusCodes = new LinkedList<>();
- for (int counter = 0; counter < ids.size(); counter++) {
- ((LinkedList<StatusCode>) statusCodes).push(StatusCode.BAD);
- }
- }
-
- for (int counter = 0; counter < names.size(); counter++) {
- final PlcResponseCode resultCode;
- if (statusCodes != null && statusCodes.size() > counter) {
- Optional<String[]> status = StatusCodes.lookup(statusCodes.get(counter).getValue());
- if (status.isPresent()) {
- if (status.get()[0].equals("Good")) {
- resultCode = PlcResponseCode.OK;
- } else if (status.get()[0].equals("Uncertain")) {
- resultCode = PlcResponseCode.NOT_FOUND;
- } else if (status.get()[0].equals("Bad")) {
- resultCode = PlcResponseCode.INVALID_DATATYPE;
- } else if (status.get()[0].equals("Bad_NodeIdUnknown")) {
- resultCode = PlcResponseCode.NOT_FOUND;
- } else {
- resultCode = PlcResponseCode.ACCESS_DENIED;
- }
- } else {
- resultCode = PlcResponseCode.ACCESS_DENIED;
- }
- } else {
- resultCode = PlcResponseCode.ACCESS_DENIED;
- }
- fieldResponse.put(names.get(counter), resultCode);
- }
- PlcWriteResponse response = new DefaultPlcWriteResponse(writeRequest, 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<EndpointDescription> endpointFilter() {
- return e -> true;
- }
-
- private SecurityPolicy getSecurityPolicy() {
- return SecurityPolicy.None;
- }
-
- private IdentityProvider getIdentityProvider() {
- return new AnonymousProvider();
- }
-
- private static String getSubPathOfParams(String params){
- if(params.contains("=")){
- if(params.contains("?")){
- return params.split("\\?")[0];
- }else{
- return "";
- }
-
- }else {
- return params;
- }
- }
-
- private static String getOptionString(String params){
- if(params.contains("=")){
- if(params.contains("?")){
- return params.split("\\?")[1];
- }else{
- return params;
- }
-
- }else {
- return "";
- }
- }
-}
diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/CertificateGenerator.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/CertificateGenerator.java
new file mode 100644
index 0000000..e545baa
--- /dev/null
+++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/CertificateGenerator.java
@@ -0,0 +1,126 @@
+/*
+ * 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.context;
+
+import org.apache.commons.lang3.RandomUtils;
+import org.bouncycastle.asn1.DERSequence;
+import org.bouncycastle.asn1.x500.X500NameBuilder;
+import org.bouncycastle.asn1.x500.style.BCStyle;
+import org.bouncycastle.asn1.x509.*;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+
+public class CertificateGenerator<PKCS10CertificateRequest> {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(CertificateGenerator.class);
+ private static final String APPURI = "urn:eclipse:milo:plc4x:server";
+
+ public static CertificateKeyPair generateCertificate() {
+ KeyPairGenerator kpg = null;
+ try {
+ kpg = KeyPairGenerator.getInstance("RSA");
+ } catch (NoSuchAlgorithmException e) {
+ LOGGER.error("Security Algorithim is unsupported for certificate");
+ }
+ kpg.initialize(2048);
+ KeyPair caKeys = kpg.generateKeyPair();
+ KeyPair userKeys = kpg.generateKeyPair();
+
+ X500NameBuilder nameBuilder = new X500NameBuilder();
+
+ nameBuilder.addRDN(BCStyle.CN, "Apache PLC4X Driver Client");
+ nameBuilder.addRDN(BCStyle.O, "Apache Software Foundation");
+ nameBuilder.addRDN(BCStyle.OU, "dev");
+ nameBuilder.addRDN(BCStyle.L, "");
+ nameBuilder.addRDN(BCStyle.ST, "DE");
+ nameBuilder.addRDN(BCStyle.C, "US");
+
+ BigInteger serial = new BigInteger(RandomUtils.nextBytes(40));
+
+ final Calendar calender = Calendar.getInstance();
+ calender.add(Calendar.DATE, -1);
+ Date startDate = calender.getTime();
+ calender.add(Calendar.DATE, 365*25);
+ Date expiryDate = calender.getTime();
+
+ KeyPairGenerator generator = null;
+ try {
+ generator = KeyPairGenerator.getInstance("RSA");
+ generator.initialize(2048, new SecureRandom());
+ KeyPair keyPair = generator.generateKeyPair();
+
+ SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(
+ keyPair.getPublic().getEncoded()
+ );
+
+ X509v3CertificateBuilder certificateBuilder = new X509v3CertificateBuilder(
+ nameBuilder.build(),
+ serial,
+ startDate,
+ expiryDate,
+ Locale.ENGLISH,
+ nameBuilder.build(),
+ subjectPublicKeyInfo
+ );
+
+ GeneralName[] gnArray = new GeneralName[] {new GeneralName(GeneralName.dNSName, InetAddress.getLocalHost().getHostName()), new GeneralName(GeneralName.uniformResourceIdentifier, APPURI)};
+
+ certificateBuilder.addExtension(Extension.authorityKeyIdentifier, false, new JcaX509ExtensionUtils().createAuthorityKeyIdentifier(keyPair.getPublic()));
+ certificateBuilder.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth}));
+ certificateBuilder.addExtension(Extension.keyUsage,false, new KeyUsage(KeyUsage.dataEncipherment | KeyUsage.digitalSignature | KeyUsage.keyAgreement | KeyUsage.keyCertSign | KeyUsage.keyEncipherment | KeyUsage.nonRepudiation));
+ certificateBuilder.addExtension(Extension.basicConstraints, false, new BasicConstraints(true));
+
+ GeneralNames subjectAltNames = GeneralNames.getInstance(new DERSequence(gnArray));
+ certificateBuilder.addExtension(Extension.subjectAlternativeName, false, subjectAltNames);
+
+ ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider("BC").build(keyPair.getPrivate());
+
+ X509CertificateHolder certificateHolder = certificateBuilder.build(sigGen);
+
+ JcaX509CertificateConverter certificateConvertor = new JcaX509CertificateConverter();
+ certificateConvertor.setProvider(new BouncyCastleProvider());
+
+ CertificateKeyPair ckp = new CertificateKeyPair(keyPair, certificateConvertor.getCertificate(certificateHolder));
+
+ return ckp;
+
+ } catch (Exception e) {
+ LOGGER.error("Security Algorithm is unsupported for certificate");
+ return null;
+ }
+ }
+}
diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/CertificateKeyPair.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/CertificateKeyPair.java
new file mode 100644
index 0000000..6b33a49
--- /dev/null
+++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/CertificateKeyPair.java
@@ -0,0 +1,44 @@
+/*
+ * 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.context;
+
+import java.security.KeyPair;
+import java.security.MessageDigest;
+import java.security.cert.X509Certificate;
+
+public class CertificateKeyPair {
+
+ private final KeyPair keyPair;
+ private final X509Certificate certificate;
+ private final byte[] thumbprint;
+
+ public CertificateKeyPair(KeyPair keyPair, X509Certificate certificate) throws Exception{
+ this.keyPair = keyPair;
+ this.certificate = certificate;
+ MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
+ this.thumbprint = messageDigest.digest(this.certificate.getEncoded());
+ }
+
+ public KeyPair getKeyPair() { return keyPair; }
+
+ public X509Certificate getCertificate() { return certificate; }
+
+ public byte[] getThumbPrint() { return thumbprint; }
+}
diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/EncryptionHandler.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/EncryptionHandler.java
new file mode 100644
index 0000000..24bbe88
--- /dev/null
+++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/EncryptionHandler.java
@@ -0,0 +1,246 @@
+/*
+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.context;
+
+import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
+import org.apache.plc4x.java.opcua.protocol.OpcuaProtocolLogic;
+import org.apache.plc4x.java.opcua.readwrite.MessagePDU;
+import org.apache.plc4x.java.opcua.readwrite.OpcuaAPU;
+import org.apache.plc4x.java.opcua.readwrite.OpcuaMessageResponse;
+import org.apache.plc4x.java.opcua.readwrite.OpcuaOpenResponse;
+import org.apache.plc4x.java.opcua.readwrite.io.OpcuaAPUIO;
+import org.apache.plc4x.java.spi.generation.*;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.Cipher;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.ByteArrayInputStream;
+import java.security.*;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+
+public class EncryptionHandler {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(OpcuaProtocolLogic.class);
+
+ static {
+ // Required for SecurityPolicy.Aes256_Sha256_RsaPss
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ private X509Certificate serverCertificate;
+ private X509Certificate clientCertificate;
+ private PrivateKey clientPrivateKey;
+ private PublicKey clientPublicKey;
+ private String securitypolicy;
+
+ public EncryptionHandler(CertificateKeyPair ckp, byte[] senderCertificate, String securityPolicy) {
+ if (ckp != null) {
+ this.clientPrivateKey = ckp.getKeyPair().getPrivate();
+ this.clientPublicKey = ckp.getKeyPair().getPublic();
+ this.clientCertificate = ckp.getCertificate();
+ }
+ if (senderCertificate != null) {
+ this.serverCertificate = getCertificateX509(senderCertificate);
+ }
+ this.securitypolicy = securityPolicy;
+ }
+
+ public ReadBuffer encodeMessage(MessagePDU pdu, byte[] message) {
+ int PREENCRYPTED_BLOCK_LENGTH = 190;
+ int unencryptedLength = pdu.getLengthInBytes();
+ int openRequestLength = message.length;
+ int positionFirstBlock = unencryptedLength - openRequestLength - 8;
+ int paddingSize = PREENCRYPTED_BLOCK_LENGTH - ((openRequestLength + 256 + 1 + 8) % PREENCRYPTED_BLOCK_LENGTH);
+ int preEncryptedLength = openRequestLength + 256 + 1 + 8 + paddingSize;
+ if (preEncryptedLength % PREENCRYPTED_BLOCK_LENGTH != 0) {
+ throw new PlcRuntimeException("Pre encrypted block length " + preEncryptedLength + " isn't a multiple of the block size");
+ }
+ int numberOfBlocks = preEncryptedLength / PREENCRYPTED_BLOCK_LENGTH;
+ int encryptedLength = numberOfBlocks * 256 + positionFirstBlock;
+ WriteBufferByteBased buf = new WriteBufferByteBased(encryptedLength, true);
+ try {
+ OpcuaAPUIO.staticSerialize(buf, new OpcuaAPU(pdu));
+ byte paddingByte = (byte) paddingSize;
+ buf.writeByte(paddingByte);
+ for (int i = 0; i < paddingSize; i++) {
+ buf.writeByte(paddingByte);
+ }
+ //Writing Message Length
+ int tempPos = buf.getPos();
+ buf.setPos(4);
+ buf.writeInt(32, encryptedLength);
+ buf.setPos(tempPos);
+ byte[] signature = sign(buf.getBytes(0, unencryptedLength + paddingSize + 1));
+ //Write the signature to the end of the buffer
+ for (int i = 0; i < signature.length; i++) {
+ buf.writeByte(signature[i]);
+ }
+ buf.setPos(positionFirstBlock);
+ encryptBlock(buf, buf.getBytes(positionFirstBlock, positionFirstBlock + preEncryptedLength));
+ return new ReadBufferByteBased(buf.getData(), true);
+ } catch (ParseException e) {
+ throw new PlcRuntimeException("Unable to parse apu prior to encrypting");
+ }
+ }
+
+ public OpcuaAPU decodeMessage(OpcuaAPU pdu) {
+ LOGGER.info("Decoding Message with Security policy {}", securitypolicy);
+ switch (securitypolicy) {
+ case "None":
+ return pdu;
+ case "Basic256Sha256":
+ byte[] message;
+ if (pdu.getMessage() instanceof OpcuaOpenResponse) {
+ message = ((OpcuaOpenResponse) pdu.getMessage()).getMessage();
+ } else if (pdu.getMessage() instanceof OpcuaMessageResponse) {
+ message = ((OpcuaMessageResponse) pdu.getMessage()).getMessage();
+ } else {
+ return pdu;
+ }
+ try {
+ int encryptedLength = pdu.getLengthInBytes();
+ int encryptedMessageLength = message.length + 8;
+ int headerLength = encryptedLength - encryptedMessageLength;
+ int numberOfBlocks = encryptedMessageLength / 256;
+ WriteBufferByteBased buf = new WriteBufferByteBased(headerLength + numberOfBlocks * 256, true);
+ OpcuaAPUIO.staticSerialize(buf, pdu);
+ byte[] data = buf.getBytes(headerLength, encryptedLength);
+ buf.setPos(headerLength);
+ decryptBlock(buf, data);
+ int tempPos = buf.getPos();
+ buf.setPos(0);
+ if (!checkSignature(buf.getBytes(0, tempPos))) {
+ LOGGER.info("Signature verification failed: - {}", buf.getBytes(0, tempPos - 256));
+ }
+ buf.setPos(4);
+ buf.writeInt(32, tempPos - 256);
+ ReadBuffer readBuffer = new ReadBufferByteBased(buf.getBytes(0, tempPos - 256), true);
+ return OpcuaAPUIO.staticParse(readBuffer, true);
+ } catch (ParseException e) {
+ LOGGER.error("Unable to Parse encrypted message");
+ }
+ }
+ return pdu;
+ }
+
+ public void decryptBlock(WriteBuffer buf, byte[] data) {
+ try {
+ Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
+ cipher.init(Cipher.DECRYPT_MODE, this.clientPrivateKey);
+
+ for (int i = 0; i < data.length; i += 256) {
+ byte[] decrypted = cipher.doFinal(data, i, 256);
+ for (int j = 0; j < 214; j++) {
+ buf.writeByte(decrypted[j]);
+ }
+ }
+ } catch (Exception e) {
+ LOGGER.error("Unable to decrypt Data");
+ e.printStackTrace();
+ }
+ }
+
+ public boolean checkSignature(byte[] data) {
+ try {
+ Signature signature = Signature.getInstance("SHA256withRSA", "BC");
+ signature.initVerify(serverCertificate.getPublicKey());
+ signature.update(data);
+ return signature.verify(data, 0, data.length - 256);
+ } catch (Exception e) {
+ e.printStackTrace();
+ LOGGER.error("Unable to sign Data");
+ return false;
+ }
+ }
+
+ public byte[] encryptPassword(byte[] data) {
+ try {
+ Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
+ cipher.init(Cipher.ENCRYPT_MODE, this.serverCertificate.getPublicKey());
+ return cipher.doFinal(data);
+ } catch (Exception e) {
+ LOGGER.error("Unable to encrypt Data");
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public void encryptBlock(WriteBuffer buf, byte[] data) {
+ try {
+ Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
+ cipher.init(Cipher.ENCRYPT_MODE, this.serverCertificate.getPublicKey());
+ for (int i = 0; i < data.length; i += 190) {
+ LOGGER.info("Iterate:- {}, Data Length:- {}", i, data.length);
+ byte[] encrypted = cipher.doFinal(data, i, 190);
+ for (int j = 0; j < 256; j++) {
+ buf.writeByte(encrypted[j]);
+ }
+ }
+ } catch (Exception e) {
+ LOGGER.error("Unable to encrypt Data");
+ e.printStackTrace();
+ }
+ }
+
+ public void encryptHmacBlock(WriteBuffer buf, byte[] data) {
+ try {
+ Mac cipher = Mac.getInstance("HmacSHA256");
+ SecretKeySpec keySpec = new SecretKeySpec(getSecretKey(), "HmacSHA256");
+ cipher.init(keySpec);
+ } catch (Exception e) {
+ LOGGER.error("Unable to encrypt Data");
+ e.printStackTrace();
+ }
+ }
+
+ public byte[] getSecretKey() {
+ return null;
+ }
+
+ public X509Certificate getCertificateX509(byte[] senderCertificate) {
+ try {
+ CertificateFactory factory = CertificateFactory.getInstance("X.509");
+ LOGGER.info("Public Key Length {}", senderCertificate.length);
+ return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(senderCertificate));
+ } catch (Exception e) {
+ LOGGER.error("Unable to get certificate from String {}", senderCertificate);
+ return null;
+ }
+ }
+
+ public byte[] sign(byte[] data) {
+ try {
+ Signature signature = Signature.getInstance("SHA256withRSA", "BC");
+ signature.initSign(this.clientPrivateKey);
+ signature.update(data);
+ byte[] ss = signature.sign();
+ LOGGER.info("----------------Signature Length{}", ss.length);
+ return ss;
+ } catch (Exception e) {
+ e.printStackTrace();
+ LOGGER.error("Unable to sign Data");
+ return null;
+ }
+ }
+}
diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SecureChannel.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SecureChannel.java
new file mode 100644
index 0000000..a05069c
--- /dev/null
+++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SecureChannel.java
@@ -0,0 +1,1199 @@
+/*
+ * 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.context;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.commons.lang3.RandomUtils;
+import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
+import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
+import org.apache.plc4x.java.opcua.config.OpcuaConfiguration;
+import org.apache.plc4x.java.opcua.readwrite.*;
+import org.apache.plc4x.java.opcua.readwrite.io.ExtensionObjectIO;
+import org.apache.plc4x.java.opcua.readwrite.io.OpcuaAPUIO;
+import org.apache.plc4x.java.opcua.readwrite.types.*;
+import org.apache.plc4x.java.spi.ConversationContext;
+import org.apache.plc4x.java.spi.context.DriverContext;
+import org.apache.plc4x.java.spi.generation.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateEncodingException;
+import java.time.Duration;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+public class SecureChannel {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SecureChannel.class);
+ private static final String FINAL_CHUNK = "F";
+ private static final String CONTINUATION_CHUNK = "C";
+ private static final String ABORT_CHUNK = "A";
+ private static final int VERSION = 0;
+ private static final int DEFAULT_MAX_CHUNK_COUNT = 64;
+ private static final int DEFAULT_MAX_MESSAGE_SIZE = 2097152;
+ private static final int DEFAULT_RECEIVE_BUFFER_SIZE = 65535;
+ private static final int DEFAULT_SEND_BUFFER_SIZE = 65535;
+ public static final Duration REQUEST_TIMEOUT = Duration.ofMillis(1000000);
+ public static final long REQUEST_TIMEOUT_LONG = 10000L;
+ private static final String PASSWORD_ENCRYPTION_ALGORITHM = "http://www.w3.org/2001/04/xmlenc#rsa-oaep";
+ private static final PascalString SECURITY_POLICY_NONE = new PascalString("http://opcfoundation.org/UA/SecurityPolicy#None");
+ protected static final PascalString NULL_STRING = new PascalString( "");
+ private static final PascalByteString NULL_BYTE_STRING = new PascalByteString( -1, null);
+ private static ExpandedNodeId NULL_EXPANDED_NODEID = new ExpandedNodeId(false,
+ false,
+ new NodeIdTwoByte((short) 0),
+ null,
+ null
+ );
+
+ protected static final ExtensionObject NULL_EXTENSION_OBJECT = new ExtensionObject(
+ NULL_EXPANDED_NODEID,
+ new ExtensionObjectEncodingMask(false, false, false),
+ new NullExtension()); // Body
+
+ private static final long EPOCH_OFFSET = 116444736000000000L; //Offset between OPC UA epoch time and linux epoch time.
+ private static final PascalString APPLICATION_URI = new PascalString("urn:apache:plc4x:client");
+ private static final PascalString PRODUCT_URI = new PascalString("urn:apache:plc4x:client");
+ private static final PascalString APPLICATION_TEXT = new PascalString("OPCUA client for the Apache PLC4X:PLC4J project");
+ private static final long DEFAULT_CONNECTION_LIFETIME = 36000000;
+ private final String sessionName = "UaSession:" + APPLICATION_TEXT.getStringValue() + ":" + RandomStringUtils.random(20, true, true);
+ private final byte[] clientNonce = RandomUtils.nextBytes(40);
+ private AtomicInteger requestHandleGenerator = new AtomicInteger(1);
+ private PascalString policyId;
+ private PascalString endpoint;
+ private boolean discovery;
+ private String username;
+ private String password;
+ private String certFile;
+ private String securityPolicy;
+ private String keyStoreFile;
+ private CertificateKeyPair ckp;
+ private PascalByteString publicCertificate;
+ private PascalByteString thumbprint;
+ private boolean isEncrypted;
+ private byte[] senderCertificate = null;
+ private byte[] senderNonce = null;
+ private PascalByteString certificateThumbprint = null;
+ private boolean checkedEndpoints = false;
+ private EncryptionHandler encryptionHandler = null;
+ private OpcuaConfiguration configuration;
+ private AtomicInteger channelId = new AtomicInteger(1);
+ private AtomicInteger tokenId = new AtomicInteger(1);
+ private NodeIdTypeDefinition authenticationToken = new NodeIdTwoByte((short) 0);
+ private DriverContext driverContext;
+ ConversationContext<OpcuaAPU> context;
+ private SecureChannelTransactionManager channelTransactionManager = new SecureChannelTransactionManager();
+ private long lifetime = DEFAULT_CONNECTION_LIFETIME;
+ private CompletableFuture<Void> keepAlive;
+ private int sendBufferSize;
+ private int maxMessageSize;
+ private long senderSequenceNumber;
+
+ public SecureChannel(DriverContext driverContext, OpcuaConfiguration configuration) {
+ this.driverContext = driverContext;
+ this.configuration = configuration;
+
+ this.endpoint = new PascalString(configuration.getEndpoint());
+ this.discovery = configuration.isDiscovery();
+ this.username = configuration.getUsername();
+ this.password = configuration.getPassword();
+ this.certFile = configuration.getCertDirectory();
+ this.securityPolicy = "http://opcfoundation.org/UA/SecurityPolicy#" + configuration.getSecurityPolicy();
+ this.ckp = configuration.getCertificateKeyPair();
+
+ if (configuration.getSecurityPolicy() != null && configuration.getSecurityPolicy().equals("Basic256Sha256")) {
+ //Sender Certificate gets populated during the discover phase when encryption is enabled.
+ this.senderCertificate = configuration.getSenderCertificate();
+ this.encryptionHandler = new EncryptionHandler(this.ckp, this.senderCertificate, configuration.getSecurityPolicy());
+ try {
+ this.publicCertificate = new PascalByteString(this.ckp.getCertificate().getEncoded().length, this.ckp.getCertificate().getEncoded());
+ this.isEncrypted = true;
+ } catch (CertificateEncodingException e) {
+ throw new PlcRuntimeException("Failed to encode the certificate");
+ }
+ this.thumbprint = configuration.getThumbprint();
+ } else {
+ this.encryptionHandler = new EncryptionHandler(this.ckp, this.senderCertificate, configuration.getSecurityPolicy());
+ this.publicCertificate = NULL_BYTE_STRING;
+ this.thumbprint = NULL_BYTE_STRING;
+ this.isEncrypted = false;
+ }
+ this.keyStoreFile = configuration.getKeyStoreFile();
+ }
+
+ public void submit(ConversationContext<OpcuaAPU> context, Consumer<TimeoutException> onTimeout, BiConsumer<OpcuaAPU, Throwable> error, Consumer<byte[]> consumer, WriteBufferByteBased buffer) {
+ int transactionId = channelTransactionManager.getTransactionIdentifier();
+
+ OpcuaMessageRequest messageRequest = new OpcuaMessageRequest(FINAL_CHUNK,
+ channelId.get(),
+ tokenId.get(),
+ transactionId,
+ transactionId,
+ buffer.getData());
+
+ final OpcuaAPU apu;
+ try {
+ if (this.isEncrypted) {
+ apu = OpcuaAPUIO.staticParse(encryptionHandler.encodeMessage(messageRequest, buffer.getData()), false);
+ } else {
+ apu = new OpcuaAPU(messageRequest);
+ }
+ } catch (ParseException e) {
+ throw new PlcRuntimeException("Unable to encrypt message before sending");
+ }
+
+ Consumer<Integer> requestConsumer = t -> {
+ try {
+ ByteArrayOutputStream messageBuffer = new ByteArrayOutputStream();
+ context.sendRequest(apu)
+ .expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT)
+ .onTimeout(onTimeout)
+ .onError(error)
+ .unwrap(apuMessage -> encryptionHandler.decodeMessage(apuMessage))
+ .unwrap(p -> (OpcuaMessageResponse) p.getMessage())
+ .check(p -> {
+ if (p.getRequestId() == transactionId) {
+ try {
+ messageBuffer.write(p.getMessage());
+ } catch (IOException e) {
+ LOGGER.debug("Failed to store incoming message in buffer {}");
+ throw new PlcRuntimeException("Error while sending message");
+ }
+ if (p.getChunk().equals(FINAL_CHUNK)) {
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ })
+ .handle(opcuaResponse -> {
+ if (opcuaResponse.getChunk().equals(FINAL_CHUNK)) {
+ tokenId.set(opcuaResponse.getSecureTokenId());
+ channelId.set(opcuaResponse.getSecureChannelId());
+
+ if (!(transactionId == (opcuaResponse.getSequenceNumber() + 1))) {
+ LOGGER.error("Sequence number isn't as expected, we might have missed a packet. - {} != {}", transactionId, opcuaResponse.getSequenceNumber() + 1);
+ context.fireDisconnected();
+ }
+ consumer.accept(messageBuffer.toByteArray());
+ }
+ });
+ } catch (Exception e) {
+ throw new PlcRuntimeException("Error while sending message");
+ }
+ };
+ LOGGER.debug("Submitting Transaction to TransactionManager {}", transactionId);
+ channelTransactionManager.submit(requestConsumer, transactionId);
+ }
+
+ public void onConnect(ConversationContext<OpcuaAPU> context) {
+ // Only the TCP transport supports login.
+ LOGGER.debug("Opcua Driver running in ACTIVE mode.");
+ this.context = context;
+
+ OpcuaHelloRequest hello = new OpcuaHelloRequest(FINAL_CHUNK,
+ VERSION,
+ DEFAULT_RECEIVE_BUFFER_SIZE,
+ DEFAULT_SEND_BUFFER_SIZE,
+ DEFAULT_MAX_MESSAGE_SIZE,
+ DEFAULT_MAX_CHUNK_COUNT,
+ this.endpoint);
+
+ Consumer<Integer> requestConsumer = t -> {
+ context.sendRequest(new OpcuaAPU(hello))
+ .expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT)
+ .check(p -> p.getMessage() instanceof OpcuaAcknowledgeResponse)
+ .unwrap(p -> (OpcuaAcknowledgeResponse) p.getMessage())
+ .handle(opcuaAcknowledgeResponse -> {
+ sendBufferSize = Math.min(opcuaAcknowledgeResponse.getReceiveBufferSize(), DEFAULT_SEND_BUFFER_SIZE);
+ maxMessageSize = Math.min(opcuaAcknowledgeResponse.getMaxMessageSize(), DEFAULT_MAX_MESSAGE_SIZE);
+ onConnectOpenSecureChannel(context, opcuaAcknowledgeResponse);
+ });
+ };
+ channelTransactionManager.submit(requestConsumer, channelTransactionManager.getTransactionIdentifier());
+ }
+
+ public void onConnectOpenSecureChannel(ConversationContext<OpcuaAPU> context, OpcuaAcknowledgeResponse opcuaAcknowledgeResponse) {
+
+ int transactionId = channelTransactionManager.getTransactionIdentifier();
+
+ RequestHeader requestHeader = new RequestHeader(new NodeId(authenticationToken),
+ getCurrentDateTime(),
+ 0L, //RequestHandle
+ 0L,
+ NULL_STRING,
+ REQUEST_TIMEOUT_LONG,
+ NULL_EXTENSION_OBJECT);
+
+ OpenSecureChannelRequest openSecureChannelRequest = null;
+ if (this.isEncrypted) {
+ openSecureChannelRequest = new OpenSecureChannelRequest(
+ requestHeader,
+ VERSION,
+ SecurityTokenRequestType.securityTokenRequestTypeIssue,
+ MessageSecurityMode.messageSecurityModeSignAndEncrypt,
+ new PascalByteString(clientNonce.length, clientNonce),
+ lifetime);
+ } else {
+ openSecureChannelRequest = new OpenSecureChannelRequest(
+ requestHeader,
+ VERSION,
+ SecurityTokenRequestType.securityTokenRequestTypeIssue,
+ MessageSecurityMode.messageSecurityModeNone,
+ NULL_BYTE_STRING,
+ lifetime);
+ }
+
+ ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
+ false, //Server Index Specified
+ new NodeIdFourByte((short) 0, Integer.valueOf(openSecureChannelRequest.getIdentifier())),
+ null,
+ null);
+
+ ExtensionObject extObject = new ExtensionObject(
+ expandedNodeId,
+ null,
+ openSecureChannelRequest);
+
+ try {
+ WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), true);
+ ExtensionObjectIO.staticSerialize(buffer, extObject);
+
+ OpcuaOpenRequest openRequest = new OpcuaOpenRequest(FINAL_CHUNK,
+ 0,
+ new PascalString(this.securityPolicy),
+ this.publicCertificate,
+ this.thumbprint,
+ transactionId,
+ transactionId,
+ buffer.getData());
+
+ final OpcuaAPU apu;
+
+ if (this.isEncrypted) {
+ apu = OpcuaAPUIO.staticParse(encryptionHandler.encodeMessage(openRequest, buffer.getData()), false);
+ } else {
+ apu = new OpcuaAPU(openRequest);
+ }
+
+ Consumer<Integer> requestConsumer = t -> {
+ context.sendRequest(apu)
+ .expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT)
+ .unwrap(apuMessage -> encryptionHandler.decodeMessage(apuMessage))
+ .check(p -> p.getMessage() instanceof OpcuaOpenResponse)
+ .unwrap(p -> (OpcuaOpenResponse) p.getMessage())
+ .check(p -> {
+ if (p.getRequestId() == transactionId) {
+ return true;
+ } else {
+ return false;
+ }
+ })
+ .handle(opcuaOpenResponse -> {
+ try {
+ ReadBuffer readBuffer = new ReadBufferByteBased(opcuaOpenResponse.getMessage(), true);
+ ExtensionObject message = ExtensionObjectIO.staticParse(readBuffer, false);
+ //Store the initial sequence number from the server. there's no requirement for the server and client to use the same starting number.
+ senderSequenceNumber = opcuaOpenResponse.getSequenceNumber();
+ certificateThumbprint = opcuaOpenResponse.getReceiverCertificateThumbprint();
+
+ if (message.getBody() instanceof ServiceFault) {
+ ServiceFault fault = (ServiceFault) message.getBody();
+ LOGGER.error("Failed to connect to opc ua server for the following reason:- {}, {}", ((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode(), OpcuaStatusCode.enumForValue(((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode()));
+ } else {
+ LOGGER.debug("Got Secure Response Connection Response");
+ try {
+ OpenSecureChannelResponse openSecureChannelResponse = (OpenSecureChannelResponse) message.getBody();
+ tokenId.set((int) ((ChannelSecurityToken) openSecureChannelResponse.getSecurityToken()).getTokenId());
+ channelId.set((int) ((ChannelSecurityToken) openSecureChannelResponse.getSecurityToken()).getChannelId());
+ onConnectCreateSessionRequest(context);
+ } catch (PlcConnectionException e) {
+ LOGGER.error("Error occurred while connecting to OPC UA server");
+ e.printStackTrace();
+ }
+ }
+ } catch (ParseException e) {
+ e.printStackTrace();
+ }
+ });
+ };
+ LOGGER.debug("Submitting OpenSecureChannel with id of {}", transactionId);
+ channelTransactionManager.submit(requestConsumer, transactionId);
+ } catch (ParseException e) {
+ LOGGER.error("Unable to to Parse Open Secure Request");
+ }
+ }
+
+ public void onConnectCreateSessionRequest(ConversationContext<OpcuaAPU> context) throws PlcConnectionException {
+
+ RequestHeader requestHeader = new RequestHeader(new NodeId(authenticationToken),
+ getCurrentDateTime(),
+ 0L,
+ 0L,
+ NULL_STRING,
+ REQUEST_TIMEOUT_LONG,
+ NULL_EXTENSION_OBJECT);
+
+ LocalizedText applicationName = new LocalizedText(
+ true,
+ true,
+ new PascalString("en"),
+ APPLICATION_TEXT);
+
+ PascalString gatewayServerUri = NULL_STRING;
+ PascalString discoveryProfileUri = NULL_STRING;
+ int noOfDiscoveryUrls = -1;
+ PascalString[] discoveryUrls = new PascalString[0];
+
+ ApplicationDescription clientDescription = new ApplicationDescription(APPLICATION_URI,
+ PRODUCT_URI,
+ applicationName,
+ ApplicationType.applicationTypeClient,
+ gatewayServerUri,
+ discoveryProfileUri,
+ noOfDiscoveryUrls,
+ discoveryUrls);
+
+ CreateSessionRequest createSessionRequest = new CreateSessionRequest(
+ requestHeader,
+ clientDescription,
+ NULL_STRING,
+ this.endpoint,
+ new PascalString(sessionName),
+ new PascalByteString(clientNonce.length, clientNonce),
+ NULL_BYTE_STRING,
+ 120000L,
+ 0L);
+
+ ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
+ false, //Server Index Specified
+ new NodeIdFourByte((short) 0, Integer.valueOf(createSessionRequest.getIdentifier())),
+ null,
+ null);
+
+ ExtensionObject extObject = new ExtensionObject(
+ expandedNodeId,
+ null,
+ createSessionRequest);
+
+ try {
+ WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), true);
+ ExtensionObjectIO.staticSerialize(buffer, extObject);
+
+ Consumer<byte[]> consumer = opcuaResponse -> {
+ try {
+ ExtensionObject message = ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaResponse, true), false);
+ if (message.getBody() instanceof ServiceFault) {
+ ServiceFault fault = (ServiceFault) message.getBody();
+ LOGGER.error("Failed to connect to opc ua server for the following reason:- {}, {}", ((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode(), OpcuaStatusCode.enumForValue(((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode()));
+ } else {
+ LOGGER.debug("Got Create Session Response Connection Response");
+ try {
+ CreateSessionResponse responseMessage;
+
+ ExtensionObjectDefinition unknownExtensionObject = ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaResponse, true), false).getBody();
+ if (unknownExtensionObject instanceof CreateSessionResponse) {
+ responseMessage = (CreateSessionResponse) unknownExtensionObject;
+
+ authenticationToken = responseMessage.getAuthenticationToken().getNodeId();
+
+ onConnectActivateSessionRequest(context, responseMessage, (CreateSessionResponse) message.getBody());
+ } else {
+ ServiceFault serviceFault = (ServiceFault) unknownExtensionObject;
+ ResponseHeader header = (ResponseHeader) serviceFault.getResponseHeader();
+ LOGGER.error("Subscription ServiceFault returned from server with error code, '{}'", header.getServiceResult().toString());
+
+ }
+
+ } catch (PlcConnectionException e) {
+ LOGGER.error("Error occurred while connecting to OPC UA server");
+ } catch (ParseException e) {
+ LOGGER.error("Unable to parse the returned Subscription response");
+ e.printStackTrace();
+ }
+ }
+ } catch (ParseException e) {
+ e.printStackTrace();
+ }
+
+ };
+
+ Consumer<TimeoutException> timeout = e -> {
+ LOGGER.error("Timeout while waiting for subscription response");
+ e.printStackTrace();
+ };
+
+ BiConsumer<OpcuaAPU, Throwable> error = (message, e) -> {
+ LOGGER.error("Error while waiting for subscription response");
+ e.printStackTrace();
+ };
+
+ submit(context, timeout, error, consumer, buffer);
+ } catch (ParseException e) {
+ LOGGER.error("Unable to to Parse Create Session Request");
+ }
+ }
+
+ private void onConnectActivateSessionRequest(ConversationContext<OpcuaAPU> context, CreateSessionResponse opcuaMessageResponse, CreateSessionResponse sessionResponse) throws PlcConnectionException {
+
+ senderCertificate = sessionResponse.getServerCertificate().getStringValue();
+ senderNonce = sessionResponse.getServerNonce().getStringValue();
+ UserTokenType tokenType = UserTokenType.userTokenTypeAnonymous;
+
+ for (ExtensionObjectDefinition extensionObject: sessionResponse.getServerEndpoints()) {
+ EndpointDescription endpointDescription = (EndpointDescription) extensionObject;
+ if (endpointDescription.getEndpointUrl().getStringValue().equals(this.endpoint.getStringValue())) {
+ for (ExtensionObjectDefinition userTokenCast : endpointDescription.getUserIdentityTokens()) {
+ UserTokenPolicy identityToken = (UserTokenPolicy) userTokenCast;
+ if ((identityToken.getTokenType() == UserTokenType.userTokenTypeAnonymous) && (this.username == null)) {
+ LOGGER.info("Using Endpoint {} with security {}", endpointDescription.getEndpointUrl().getStringValue(), identityToken.getPolicyId().getStringValue());
+ policyId = identityToken.getPolicyId();
+ tokenType = identityToken.getTokenType();
+ } else if ((identityToken.getTokenType() == UserTokenType.userTokenTypeUserName) && (this.username != null)) {
+ LOGGER.info("Using Endpoint {} with security {}", endpointDescription.getEndpointUrl().getStringValue(), identityToken.getPolicyId().getStringValue());
+ policyId = identityToken.getPolicyId();
+ tokenType = identityToken.getTokenType();
+ }
+ }
+ }
+ }
+
+ ExtensionObject userIdentityToken = getIdentityToken(tokenType, policyId.getStringValue());
+
+ int requestHandle = getRequestHandle();
+
+ RequestHeader requestHeader = new RequestHeader(new NodeId(authenticationToken),
+ getCurrentDateTime(),
+ requestHandle,
+ 0L,
+ NULL_STRING,
+ REQUEST_TIMEOUT_LONG,
+ NULL_EXTENSION_OBJECT);
+
+ SignatureData clientSignature = new SignatureData(NULL_STRING, NULL_BYTE_STRING);
+
+ SignedSoftwareCertificate[] signedSoftwareCertificate = new SignedSoftwareCertificate[1];
+
+ signedSoftwareCertificate[0] = new SignedSoftwareCertificate(NULL_BYTE_STRING, NULL_BYTE_STRING);
+
+ ActivateSessionRequest activateSessionRequest = new ActivateSessionRequest(
+ requestHeader,
+ clientSignature,
+ 0,
+ null,
+ 0,
+ null,
+ userIdentityToken,
+ clientSignature);
+
+ ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
+ false, //Server Index Specified
+ new NodeIdFourByte((short) 0, Integer.valueOf(activateSessionRequest.getIdentifier())),
+ null,
+ null);
+
+ ExtensionObject extObject = new ExtensionObject(
+ expandedNodeId,
+ null,
+ activateSessionRequest);
+
+ try {
+ WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), true);
+ ExtensionObjectIO.staticSerialize(buffer, extObject);
+
+ Consumer<byte[]> consumer = opcuaResponse -> {
+ try {
+ ExtensionObject message = ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaResponse, true), false);
+ if (message.getBody() instanceof ServiceFault) {
+ ServiceFault fault = (ServiceFault) message.getBody();
+ LOGGER.error("Failed to connect to opc ua server for the following reason:- {}, {}", ((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode(), OpcuaStatusCode.enumForValue(((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode()));
+ } else {
+ LOGGER.debug("Got Activate Session Response Connection Response");
+ try {
+ ActivateSessionResponse responseMessage;
+
+ ExtensionObjectDefinition unknownExtensionObject = ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaResponse, true), false).getBody();
+ if (unknownExtensionObject instanceof ActivateSessionResponse) {
+ responseMessage = (ActivateSessionResponse) unknownExtensionObject;
+
+ long returnedRequestHandle = ((ResponseHeader) responseMessage.getResponseHeader()).getRequestHandle();
+ if (!(requestHandle == returnedRequestHandle)) {
+ LOGGER.error("Request handle isn't as expected, we might have missed a packet. {} != {}", requestHandle, returnedRequestHandle);
+ }
+
+ // Send an event that connection setup is complete.
+ keepAlive();
+ context.fireConnected();
+ } else {
+ ServiceFault serviceFault = (ServiceFault) unknownExtensionObject;
+ ResponseHeader header = (ResponseHeader) serviceFault.getResponseHeader();
+ LOGGER.error("Subscription ServiceFault returned from server with error code, '{}'", header.getServiceResult().toString());
+ }
+ } catch (ParseException e) {
+ LOGGER.error("Unable to parse the returned Subscription response");
+ e.printStackTrace();
+ }
+ }
+ } catch (ParseException e) {
+ e.printStackTrace();
+ }
+
+ };
+
+ Consumer<TimeoutException> timeout = e -> {
+ LOGGER.error("Timeout while waiting for activate session response");
+ e.printStackTrace();
+ };
+
+ BiConsumer<OpcuaAPU, Throwable> error = (message, e) -> {
+ LOGGER.error("Error while waiting for activate session response");
+ e.printStackTrace();
+ };
+
+ submit(context, timeout, error, consumer, buffer);
+ } catch (ParseException e) {
+ LOGGER.error("Unable to to Parse Activate Session Request");
+ }
+ }
+
+ public void onDisconnect(ConversationContext<OpcuaAPU> context) {
+ LOGGER.info("Disconnecting");
+ int requestHandle = getRequestHandle();
+
+ if (keepAlive != null) {
+ keepAlive.complete(null);
+ }
+
+ ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
+ false, //Server Index Specified
+ new NodeIdFourByte((short) 0, 473),
+ null,
+ null); //Identifier for OpenSecureChannel
+
+ RequestHeader requestHeader = new RequestHeader(
+ new NodeId(authenticationToken),
+ getCurrentDateTime(),
+ requestHandle, //RequestHandle
+ 0L,
+ NULL_STRING,
+ 5000L,
+ NULL_EXTENSION_OBJECT);
+
+ CloseSessionRequest closeSessionRequest = new CloseSessionRequest(
+ requestHeader,
+ true);
+
+ ExtensionObject extObject = new ExtensionObject(
+ expandedNodeId,
+ null,
+ closeSessionRequest);
+
+ try {
+ WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), true);
+ ExtensionObjectIO.staticSerialize(buffer, extObject);
+
+ Consumer<byte[]> consumer = opcuaResponse -> {
+ try {
+ ExtensionObject message = ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaResponse, true), false);
+ if (message.getBody() instanceof ServiceFault) {
+ ServiceFault fault = (ServiceFault) message.getBody();
+ LOGGER.error("Failed to connect to opc ua server for the following reason:- {}, {}", ((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode(), OpcuaStatusCode.enumForValue(((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode()));
+ } else {
+ LOGGER.debug("Got Close Session Response Connection Response");
+ try {
+ CloseSessionResponse responseMessage;
+
+ ExtensionObjectDefinition unknownExtensionObject = ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaResponse, true), false).getBody();
+ if (unknownExtensionObject instanceof CloseSessionResponse) {
+ responseMessage = (CloseSessionResponse) unknownExtensionObject;
+
+ LOGGER.trace("Got Close Session Response Connection Response" + responseMessage.toString());
+ onDisconnectCloseSecureChannel(context);
+ } else {
+ ServiceFault serviceFault = (ServiceFault) unknownExtensionObject;
+ ResponseHeader header = (ResponseHeader) serviceFault.getResponseHeader();
+ LOGGER.error("Subscription ServiceFault returned from server with error code, '{}'", header.getServiceResult().toString());
+ }
+ } catch (ParseException e) {
+ LOGGER.error("Unable to parse the returned Close Session response");
+ e.printStackTrace();
+ }
+ }
+ } catch (ParseException e) {
+ e.printStackTrace();
+ }
+
+ };
+
+ Consumer<TimeoutException> timeout = e -> {
+ LOGGER.error("Timeout while waiting for close session response");
+ e.printStackTrace();
+ };
+
+ BiConsumer<OpcuaAPU, Throwable> error = (message, e) -> {
+ LOGGER.error("Error while waiting for close session response");
+ e.printStackTrace();
+ };
+
+ submit(context, timeout, error, consumer, buffer);
+ } catch (ParseException e) {
+ LOGGER.error("Unable to to Parse Close Session Request");
+ }
+ }
+
+ private void onDisconnectCloseSecureChannel(ConversationContext<OpcuaAPU> context) {
+
+ int transactionId = channelTransactionManager.getTransactionIdentifier();
+
+ RequestHeader requestHeader = new RequestHeader(new NodeId(authenticationToken),
+ getCurrentDateTime(),
+ 0L, //RequestHandle
+ 0L,
+ NULL_STRING,
+ REQUEST_TIMEOUT_LONG,
+ NULL_EXTENSION_OBJECT);
+
+ CloseSecureChannelRequest closeSecureChannelRequest = new CloseSecureChannelRequest(requestHeader);
+
+ ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
+ false, //Server Index Specified
+ new NodeIdFourByte((short) 0, Integer.valueOf(closeSecureChannelRequest.getIdentifier())),
+ null,
+ null);
+
+ OpcuaCloseRequest closeRequest = new OpcuaCloseRequest(FINAL_CHUNK,
+ channelId.get(),
+ tokenId.get(),
+ transactionId,
+ transactionId,
+ new ExtensionObject(
+ expandedNodeId,
+ null,
+ closeSecureChannelRequest));
+
+ Consumer<Integer> requestConsumer = t -> {
+ context.sendRequest(new OpcuaAPU(closeRequest))
+ .expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT)
+ .check(p -> p.getMessage() instanceof OpcuaMessageResponse)
+ .unwrap(p -> (OpcuaMessageResponse) p.getMessage())
+ .check(p -> {
+ if (p.getRequestId() == transactionId) {
+ return true;
+ } else {
+ return false;
+ }
+ })
+ .handle(opcuaMessageResponse -> {
+ LOGGER.trace("Got Close Secure Channel Response" + opcuaMessageResponse.toString());
+ });
+
+ context.fireDisconnected();
+ };
+
+ channelTransactionManager.submit(requestConsumer, transactionId);
+
+ }
+
+ public void onDiscover(ConversationContext<OpcuaAPU> context) {
+ // Only the TCP transport supports login.
+ LOGGER.debug("Opcua Driver running in ACTIVE mode, discovering endpoints");
+
+ OpcuaHelloRequest hello = new OpcuaHelloRequest(FINAL_CHUNK,
+ VERSION,
+ DEFAULT_RECEIVE_BUFFER_SIZE,
+ DEFAULT_SEND_BUFFER_SIZE,
+ DEFAULT_MAX_MESSAGE_SIZE,
+ DEFAULT_MAX_CHUNK_COUNT,
+ this.endpoint);
+
+ Consumer<Integer> requestConsumer = t -> {
+ context.sendRequest(new OpcuaAPU(hello))
+ .expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT)
+ .check(p -> p.getMessage() instanceof OpcuaAcknowledgeResponse)
+ .unwrap(p -> (OpcuaAcknowledgeResponse) p.getMessage())
+ .handle(opcuaAcknowledgeResponse -> {
+ LOGGER.debug("Got Hello Response Connection Response");
+ onDiscoverOpenSecureChannel(context, opcuaAcknowledgeResponse);
+ });
+ };
+
+ channelTransactionManager.submit(requestConsumer, 1);
+ }
+
+ public void onDiscoverOpenSecureChannel(ConversationContext<OpcuaAPU> context, OpcuaAcknowledgeResponse opcuaAcknowledgeResponse) {
+ int transactionId = channelTransactionManager.getTransactionIdentifier();
+
+ RequestHeader requestHeader = new RequestHeader(new NodeId(authenticationToken),
+ getCurrentDateTime(),
+ 0L, //RequestHandle
+ 0L,
+ NULL_STRING,
+ REQUEST_TIMEOUT_LONG,
+ NULL_EXTENSION_OBJECT);
+
+ OpenSecureChannelRequest openSecureChannelRequest = new OpenSecureChannelRequest(
+ requestHeader,
+ VERSION,
+ SecurityTokenRequestType.securityTokenRequestTypeIssue,
+ MessageSecurityMode.messageSecurityModeNone,
+ NULL_BYTE_STRING,
+ lifetime);
+
+ ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
+ false, //Server Index Specified
+ new NodeIdFourByte((short) 0, Integer.valueOf(openSecureChannelRequest.getIdentifier())),
+ null,
+ null);
+
+
+ try {
+ WriteBufferByteBased buffer = new WriteBufferByteBased(openSecureChannelRequest.getLengthInBytes(), true);
+ ExtensionObjectIO.staticSerialize(buffer, new ExtensionObject(
+ expandedNodeId,
+ null,
+ openSecureChannelRequest));
+
+ OpcuaOpenRequest openRequest = new OpcuaOpenRequest(FINAL_CHUNK,
+ 0,
+ SECURITY_POLICY_NONE,
+ NULL_BYTE_STRING,
+ NULL_BYTE_STRING,
+ transactionId,
+ transactionId,
+ buffer.getData());
+
+ Consumer<Integer> requestConsumer = t -> {
+ context.sendRequest(new OpcuaAPU(openRequest))
+ .expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT)
+ .check(p -> p.getMessage() instanceof OpcuaOpenResponse)
+ .unwrap(p -> (OpcuaOpenResponse) p.getMessage())
+ .check(p -> {
+ if (p.getRequestId() == transactionId) {
+ return true;
+ } else {
+ return false;
+ }
+ })
+ .handle(opcuaOpenResponse -> {
+ try {
+ ExtensionObject message = ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaOpenResponse.getMessage(), true), false);
+ if (message.getBody() instanceof ServiceFault) {
+ ServiceFault fault = (ServiceFault) message.getBody();
+ LOGGER.error("Failed to connect to opc ua server for the following reason:- {}, {}", ((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode(), OpcuaStatusCode.enumForValue(((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode()));
+ } else {
+ LOGGER.debug("Got Secure Response Connection Response");
+ try {
+ onDiscoverGetEndpointsRequest(context, opcuaOpenResponse, (OpenSecureChannelResponse) message.getBody());
+ } catch (PlcConnectionException e) {
+ LOGGER.error("Error occurred while connecting to OPC UA server");
+ }
+ }
+ } catch (ParseException e) {
+ e.printStackTrace();
+ }
+ });
+ };
+
+ channelTransactionManager.submit(requestConsumer, transactionId);
+ } catch (ParseException e) {
+ LOGGER.error("Unable to to Parse Create Session Request");
+ }
+ }
+
+ public void onDiscoverGetEndpointsRequest(ConversationContext<OpcuaAPU> context, OpcuaOpenResponse opcuaOpenResponse, OpenSecureChannelResponse openSecureChannelResponse) throws PlcConnectionException {
+ certificateThumbprint = opcuaOpenResponse.getReceiverCertificateThumbprint();
+ tokenId.set((int) ((ChannelSecurityToken) openSecureChannelResponse.getSecurityToken()).getTokenId());
+ channelId.set((int) ((ChannelSecurityToken) openSecureChannelResponse.getSecurityToken()).getChannelId());
+
+ int transactionId = channelTransactionManager.getTransactionIdentifier();
+
+ Integer nextSequenceNumber = opcuaOpenResponse.getSequenceNumber() + 1;
+ Integer nextRequestId = opcuaOpenResponse.getRequestId() + 1;
+
+ if (!(transactionId == nextSequenceNumber)) {
+ LOGGER.error("Sequence number isn't as expected, we might have missed a packet. - " + transactionId + " != " + nextSequenceNumber);
+ throw new PlcConnectionException("Sequence number isn't as expected, we might have missed a packet. - " + transactionId + " != " + nextSequenceNumber);
+ }
+
+ RequestHeader requestHeader = new RequestHeader(new NodeId(authenticationToken),
+ getCurrentDateTime(),
+ 0L,
+ 0L,
+ NULL_STRING,
+ REQUEST_TIMEOUT_LONG,
+ NULL_EXTENSION_OBJECT);
+
+ GetEndpointsRequest endpointsRequest = new GetEndpointsRequest(
+ requestHeader,
+ this.endpoint,
+ 0,
+ null,
+ 0,
+ null);
+
+ ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
+ false, //Server Index Specified
+ new NodeIdFourByte((short) 0, Integer.valueOf(endpointsRequest.getIdentifier())),
+ null,
+ null);
+
+ try {
+ WriteBufferByteBased buffer = new WriteBufferByteBased(endpointsRequest.getLengthInBytes(), true);
+ ExtensionObjectIO.staticSerialize(buffer, new ExtensionObject(
+ expandedNodeId,
+ null,
+ endpointsRequest));
+
+ OpcuaMessageRequest messageRequest = new OpcuaMessageRequest(FINAL_CHUNK,
+ channelId.get(),
+ tokenId.get(),
+ nextSequenceNumber,
+ nextRequestId,
+ buffer.getData());
+
+ Consumer<Integer> requestConsumer = t -> {
+ context.sendRequest(new OpcuaAPU(messageRequest))
+ .expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT)
+ .check(p -> p.getMessage() instanceof OpcuaMessageResponse)
+ .unwrap(p -> (OpcuaMessageResponse) p.getMessage())
+ .check(p -> {
+ if (p.getRequestId() == transactionId) {
+ return true;
+ } else {
+ return false;
+ }
+ })
+ .handle(opcuaMessageResponse -> {
+ try {
+ ExtensionObject message = ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaMessageResponse.getMessage(), true), false);
+ if (message.getBody() instanceof ServiceFault) {
+ ServiceFault fault = (ServiceFault) message.getBody();
+ LOGGER.error("Failed to connect to opc ua server for the following reason:- {}, {}", ((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode(), OpcuaStatusCode.enumForValue(((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode()));
+ } else {
+ LOGGER.debug("Got Create Session Response Connection Response");
+ GetEndpointsResponse response = (GetEndpointsResponse) message.getBody();
+
+ EndpointDescription[] endpoints = (EndpointDescription[]) response.getEndpoints();
+ for (EndpointDescription endpoint : endpoints) {
+ if (endpoint.getEndpointUrl().getStringValue().equals(this.endpoint.getStringValue()) && endpoint.getSecurityPolicyUri().getStringValue().equals(this.securityPolicy)) {
+ LOGGER.info("Found OPC UA endpoint {}", this.endpoint.getStringValue());
+ this.configuration.setSenderCertificate(endpoint.getServerCertificate().getStringValue());
+ }
+ }
+
+ try {
+ MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
+ byte[] digest = messageDigest.digest(this.configuration.getSenderCertificate());
+ this.configuration.setThumbprint(new PascalByteString(digest.length, digest));
+ } catch (NoSuchAlgorithmException e) {
+ LOGGER.error("Failed to find hashing algorithm");
+ }
+ onDiscoverCloseSecureChannel(context, response);
+ }
+ } catch (ParseException e) {
+ e.printStackTrace();
+ }
+ });
+ };
+
+ channelTransactionManager.submit(requestConsumer, transactionId);
+ } catch (ParseException e) {
+ LOGGER.error("Unable to to Parse Create Session Request");
+ }
+ }
+
+ private void onDiscoverCloseSecureChannel(ConversationContext<OpcuaAPU> context, GetEndpointsResponse message) {
+
+ int transactionId = channelTransactionManager.getTransactionIdentifier();
+
+ RequestHeader requestHeader = new RequestHeader(new NodeId(authenticationToken),
+ getCurrentDateTime(),
+ 0L, //RequestHandle
+ 0L,
+ NULL_STRING,
+ REQUEST_TIMEOUT_LONG,
+ NULL_EXTENSION_OBJECT);
+
+ CloseSecureChannelRequest closeSecureChannelRequest = new CloseSecureChannelRequest(requestHeader);
+
+ ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
+ false, //Server Index Specified
+ new NodeIdFourByte((short) 0, Integer.valueOf(closeSecureChannelRequest.getIdentifier())),
+ null,
+ null);
+
+ OpcuaCloseRequest closeRequest = new OpcuaCloseRequest(FINAL_CHUNK,
+ channelId.get(),
+ tokenId.get(),
+ transactionId,
+ transactionId,
+ new ExtensionObject(
+ expandedNodeId,
+ null,
+ closeSecureChannelRequest));
+
+ Consumer<Integer> requestConsumer = t -> {
+ context.sendRequest(new OpcuaAPU(closeRequest))
+ .expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT)
+ .check(p -> p.getMessage() instanceof OpcuaMessageResponse)
+ .unwrap(p -> (OpcuaMessageResponse) p.getMessage())
+ .check(p -> {
+ if (p.getRequestId() == transactionId) {
+ return true;
+ } else {
+ return false;
+ }
+ })
+ .handle(opcuaMessageResponse -> {
+ LOGGER.trace("Got Close Secure Channel Response" + opcuaMessageResponse.toString());
+ // Send an event that connection setup is complete.
+ context.fireDiscovered(this.configuration);
+ });
+ };
+
+ channelTransactionManager.submit(requestConsumer, transactionId);
+ }
+
+ private void keepAlive() {
+ keepAlive = CompletableFuture.supplyAsync(() -> {
+ while(true) {
+
+ try {
+ Thread.sleep((long) Math.ceil(this.lifetime * 0.75f));
+ } catch (InterruptedException e) {
+ LOGGER.trace("Interrupted Exception");
+ }
+
+ int transactionId = channelTransactionManager.getTransactionIdentifier();
+
+ RequestHeader requestHeader = new RequestHeader(new NodeId(authenticationToken),
+ getCurrentDateTime(),
+ 0L, //RequestHandle
+ 0L,
+ NULL_STRING,
+ REQUEST_TIMEOUT_LONG,
+ NULL_EXTENSION_OBJECT);
+
+ OpenSecureChannelRequest openSecureChannelRequest = null;
+ if (this.isEncrypted) {
+ openSecureChannelRequest = new OpenSecureChannelRequest(
+ requestHeader,
+ VERSION,
+ SecurityTokenRequestType.securityTokenRequestTypeIssue,
+ MessageSecurityMode.messageSecurityModeSignAndEncrypt,
+ new PascalByteString(clientNonce.length, clientNonce),
+ lifetime);
+ } else {
+ openSecureChannelRequest = new OpenSecureChannelRequest(
+ requestHeader,
+ VERSION,
+ SecurityTokenRequestType.securityTokenRequestTypeIssue,
+ MessageSecurityMode.messageSecurityModeNone,
+ NULL_BYTE_STRING,
+ lifetime);
+ }
+
+ ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
+ false, //Server Index Specified
+ new NodeIdFourByte((short) 0, Integer.valueOf(openSecureChannelRequest.getIdentifier())),
+ null,
+ null);
+
+ ExtensionObject extObject = new ExtensionObject(
+ expandedNodeId,
+ null,
+ openSecureChannelRequest);
+
+ try {
+ WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), true);
+ ExtensionObjectIO.staticSerialize(buffer, extObject);
+
+ OpcuaOpenRequest openRequest = new OpcuaOpenRequest(FINAL_CHUNK,
+ 0,
+ new PascalString(this.securityPolicy),
+ this.publicCertificate,
+ this.thumbprint,
+ transactionId,
+ transactionId,
+ buffer.getData());
+
+ final OpcuaAPU apu;
+
+ if (this.isEncrypted) {
+ apu = OpcuaAPUIO.staticParse(encryptionHandler.encodeMessage(openRequest, buffer.getData()), false);
+ } else {
+ apu = new OpcuaAPU(openRequest);
+ }
+
+ Consumer<Integer> requestConsumer = t -> {
+ context.sendRequest(apu)
+ .expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT)
+ .unwrap(apuMessage -> encryptionHandler.decodeMessage(apuMessage))
+ .check(p -> p.getMessage() instanceof OpcuaOpenResponse)
+ .unwrap(p -> (OpcuaOpenResponse) p.getMessage())
+ .check(p -> {
+ if (p.getRequestId() == transactionId) {
+ return true;
+ } else {
+ return false;
+ }
+ })
+ .handle(opcuaOpenResponse -> {
+ try {
+ ReadBufferByteBased readBuffer = new ReadBufferByteBased(opcuaOpenResponse.getMessage(), true);
+ ExtensionObject message = ExtensionObjectIO.staticParse(readBuffer, false);
+
+ if (message.getBody() instanceof ServiceFault) {
+ ServiceFault fault = (ServiceFault) message.getBody();
+ LOGGER.error("Failed to connect to opc ua server for the following reason:- {}, {}", ((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode(), OpcuaStatusCode.enumForValue(((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode()));
+ } else {
+ LOGGER.debug("Got Secure Response Connection Response");
+ OpenSecureChannelResponse openSecureChannelResponse = (OpenSecureChannelResponse) message.getBody();
+ ChannelSecurityToken token = (ChannelSecurityToken) openSecureChannelResponse.getSecurityToken();
+ certificateThumbprint = opcuaOpenResponse.getReceiverCertificateThumbprint();
+ tokenId.set((int) token.getTokenId());
+ channelId.set((int) token.getChannelId());
+ lifetime = token.getRevisedLifetime();
+ }
+ } catch (ParseException e) {
+ e.printStackTrace();
+ }
+ });
+ };
+ channelTransactionManager.submit(requestConsumer, transactionId);
+ } catch (ParseException e) {
+ LOGGER.error("Unable to to Parse Open Secure Request");
+ }
+ }
+ }
+ );
+ }
+
+ /**
+ * Returns the next request handle
+ *
+ * @return the next sequential request handle
+ */
+ public int getRequestHandle() {
+ int transactionId = requestHandleGenerator.getAndIncrement();
+ if(requestHandleGenerator.get() == SecureChannelTransactionManager.DEFAULT_MAX_REQUEST_ID) {
+ requestHandleGenerator.set(1);
+ }
+ return transactionId;
+ }
+
+ /**
+ * Returns the authentication token for the current connection
+ *
+ * @return a NodeId Authentication token
+ */
+ public NodeId getAuthenticationToken() {
+ return new NodeId(this.authenticationToken);
+ }
+
+ /**
+ * Gets the Channel identifier for the current channel
+ *
+ * @return int representing the channel identifier
+ */
+ public int getChannelId() {
+ return this.channelId.get();
+ }
+
+ /**
+ * Gets the Token Identifier
+ *
+ * @return int representing the token identifier
+ */
+ public int getTokenId() {
+ return this.tokenId.get();
+ }
+
+ /**
+ * Creates an IdentityToken to authenticate with a server.
+ * @param securityPolicy
+ * @return returns an ExtensionObject with an IdentityToken.
+ */
+ private ExtensionObject getIdentityToken(UserTokenType tokenType, String securityPolicy) {
+ ExpandedNodeId extExpandedNodeId = null;
+ ExtensionObject userIdentityToken = null;
+ switch (tokenType) {
+ case userTokenTypeAnonymous:
+ //If we aren't using authentication tell the server we would like to login anonymously
+ AnonymousIdentityToken anonymousIdentityToken = new AnonymousIdentityToken();
+
+ extExpandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
+ false, //Server Index Specified
+ new NodeIdFourByte((short) 0, OpcuaNodeIdServices.AnonymousIdentityToken_Encoding_DefaultBinary.getValue()),
+ null,
+ null);
+
+ return new ExtensionObject(
+ extExpandedNodeId,
+ new ExtensionObjectEncodingMask(false, false, true),
+ new UserIdentityToken(new PascalString(securityPolicy), anonymousIdentityToken));
+ case userTokenTypeUserName:
+ //Encrypt the password using the server nonce and server public key
+ byte[] passwordBytes = this.password.getBytes();
+ ByteBuffer encodeableBuffer = ByteBuffer.allocate(4 + passwordBytes.length + this.senderNonce.length);
+ encodeableBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ encodeableBuffer.putInt(passwordBytes.length + this.senderNonce.length);
+ encodeableBuffer.put(passwordBytes);
+ encodeableBuffer.put(this.senderNonce);
+ byte[] encodeablePassword = new byte[4 + passwordBytes.length + this.senderNonce.length];
+ encodeableBuffer.position(0);
+ encodeableBuffer.get(encodeablePassword);
+
+ byte[] encryptedPassword = encryptionHandler.encryptPassword(encodeablePassword);
+ UserNameIdentityToken userNameIdentityToken = new UserNameIdentityToken(
+ new PascalString(this.username),
+ new PascalByteString(encryptedPassword.length, encryptedPassword),
+ new PascalString(PASSWORD_ENCRYPTION_ALGORITHM)
+ );
+
+ extExpandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
+ false, //Server Index Specified
+ new NodeIdFourByte((short) 0, OpcuaNodeIdServices.UserNameIdentityToken_Encoding_DefaultBinary.getValue()),
+ NULL_STRING,
+ 1L);
+
+ return new ExtensionObject(
+ extExpandedNodeId,
+ new ExtensionObjectEncodingMask(false, false, true),
+ new UserIdentityToken(new PascalString(securityPolicy), userNameIdentityToken));
+ }
+ return null;
+ }
+
+ public static long getCurrentDateTime() {
+ return (System.currentTimeMillis() * 10000) + EPOCH_OFFSET;
+ }
+}
diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SecureChannelTransactionManager.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SecureChannelTransactionManager.java
new file mode 100644
index 0000000..b53cc73
--- /dev/null
+++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SecureChannelTransactionManager.java
@@ -0,0 +1,116 @@
+/*
+ * 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.context;
+
+import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
+import org.apache.plc4x.java.opcua.readwrite.OpcuaAPU;
+import org.apache.plc4x.java.opcua.readwrite.OpcuaMessageRequest;
+import org.apache.plc4x.java.opcua.readwrite.OpcuaMessageResponse;
+import org.apache.plc4x.java.spi.ConversationContext;
+import org.apache.plc4x.java.spi.context.DriverContext;
+import org.apache.plc4x.java.spi.transaction.RequestTransactionManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+public class SecureChannelTransactionManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SecureChannel.class);
+ public static final int DEFAULT_MAX_REQUEST_ID = 0xFFFFFFFF;
+ private AtomicInteger transactionIdentifierGenerator = new AtomicInteger(0);
+ private AtomicInteger requestIdentifierGenerator = new AtomicInteger(0);
+ private AtomicInteger activeTransactionId = new AtomicInteger(0);
+ private Map<Integer, Transaction> queue = new HashMap<>();
+
+ public void submit(Consumer<Integer> onSend, Integer transactionId) {
+ LOGGER.info("New Transaction Submitted {}", activeTransactionId.get());
+ if (activeTransactionId.get() == transactionId) {
+ onSend.accept(transactionId);
+ int newTransactionId = getActiveTransactionIdentifier();
+ if (!queue.isEmpty()) {
+ Transaction t = queue.remove(newTransactionId);
+ if (t == null) {
+ LOGGER.info("Length of Queue is {}", queue.size());
+ LOGGER.info("Transaction ID is {}", newTransactionId);
+ LOGGER.info("Map is {}", queue);
+ throw new PlcRuntimeException("Transaction Id not found in queued messages {}");
+ }
+ submit(t.getConsumer(), t.getTransactionId());
+ }
+ } else {
+ LOGGER.info("Storing out of order transaction {}", transactionId);
+ queue.put(transactionId, new Transaction(onSend, transactionId));
+ }
+ }
+
+ /**
+ * Returns the next transaction identifier.
+ *
+ * @return the next sequential transaction identifier
+ */
+ public int getTransactionIdentifier() {
+ int transactionId = transactionIdentifierGenerator.getAndIncrement();
+ if(transactionIdentifierGenerator.get() == DEFAULT_MAX_REQUEST_ID) {
+ transactionIdentifierGenerator.set(1);
+ }
+ return transactionId;
+ }
+
+ /**
+ * Returns the next transaction identifier.
+ *
+ * @return the next sequential transaction identifier
+ */
+ private int getActiveTransactionIdentifier() {
+ int transactionId = activeTransactionId.incrementAndGet();
+ if(activeTransactionId.get() == DEFAULT_MAX_REQUEST_ID) {
+ activeTransactionId.set(1);
+ }
+ return transactionId;
+ }
+
+ public class Transaction {
+
+ private Integer transactionId;
+ private Consumer<Integer> consumer;
+
+ public Transaction(Consumer<Integer> consumer, Integer transactionId) {
+ this.consumer = consumer;
+ this.transactionId = transactionId;
+ }
+
+ public Integer getTransactionId() {
+ return transactionId;
+ }
+
+ public Consumer<Integer> getConsumer() {
+ return consumer;
+ }
+ }
+
+}
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/field/OpcuaField.java
similarity index 90%
rename from plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaField.java
rename to plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/field/OpcuaField.java
index 7a443dd..46acce2 100644
--- 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/field/OpcuaField.java
@@ -16,7 +16,7 @@
specific language governing permissions and limitations
under the License.
*/
-package org.apache.plc4x.java.opcua.protocol;
+package org.apache.plc4x.java.opcua.field;
import org.apache.commons.lang3.EnumUtils;
import org.apache.plc4x.java.api.exceptions.PlcInvalidFieldException;
@@ -47,16 +47,6 @@
private final OpcuaDataType dataType;
- protected OpcuaField(int namespace, OpcuaIdentifierType identifierType, String identifier, OpcuaDataType dataType) {
- 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.");
- }
- this.dataType = dataType;
- }
-
private OpcuaField(Integer namespace, String identifier, OpcuaIdentifierType identifierType, OpcuaDataType dataType) {
this.identifier = Objects.requireNonNull(identifier);
this.identifierType = Objects.requireNonNull(identifierType);
@@ -89,7 +79,6 @@
return new OpcuaField(namespace, identifier, identifierType, dataType);
}
-
public static boolean matches(String address) {
return ADDRESS_PATTERN.matcher(address).matches();
}
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/field/OpcuaPlcFieldHandler.java
similarity index 88%
rename from plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaPlcFieldHandler.java
rename to plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/field/OpcuaPlcFieldHandler.java
index f6431ad..994bf51 100644
--- 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/field/OpcuaPlcFieldHandler.java
@@ -16,10 +16,9 @@
specific language governing permissions and limitations
under the License.
*/
-package org.apache.plc4x.java.opcua.protocol;
+package org.apache.plc4x.java.opcua.field;
import org.apache.plc4x.java.api.exceptions.PlcInvalidFieldException;
-import org.apache.plc4x.java.api.model.PlcField;
import org.apache.plc4x.java.spi.connection.PlcFieldHandler;
/**
@@ -27,7 +26,7 @@
public class OpcuaPlcFieldHandler implements PlcFieldHandler {
@Override
- public PlcField createField(String fieldQuery) {
+ public OpcuaField createField(String fieldQuery) {
if (OpcuaField.matches(fieldQuery)) {
return OpcuaField.of(fieldQuery);
}
diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/optimizer/OpcuaOptimizer.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/optimizer/OpcuaOptimizer.java
new file mode 100644
index 0000000..8dac3a2
--- /dev/null
+++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/optimizer/OpcuaOptimizer.java
@@ -0,0 +1,58 @@
+/*
+ * 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.optimizer;
+
+import org.apache.plc4x.java.api.messages.PlcReadRequest;
+import org.apache.plc4x.java.api.messages.PlcRequest;
+import org.apache.plc4x.java.api.model.PlcField;
+import org.apache.plc4x.java.opcua.field.OpcuaField;
+import org.apache.plc4x.java.spi.context.DriverContext;
+import org.apache.plc4x.java.spi.messages.DefaultPlcReadRequest;
+import org.apache.plc4x.java.spi.optimizer.BaseOptimizer;
+
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+public class OpcuaOptimizer extends BaseOptimizer{
+
+ @Override
+ protected List<PlcRequest> processReadRequest(PlcReadRequest readRequest, DriverContext driverContext) {
+ List<PlcRequest> processedRequests = new LinkedList<>();
+
+ // List of all items in the current request.
+ LinkedHashMap<String, PlcField> curFields = new LinkedHashMap<>();
+
+ for (String fieldName : readRequest.getFieldNames()) {
+ OpcuaField field = (OpcuaField) readRequest.getField(fieldName);
+ curFields.put(fieldName, field);
+ }
+
+ // Create a new PlcReadRequest from the remaining field items.
+ if(!curFields.isEmpty()) {
+ processedRequests.add(new DefaultPlcReadRequest(
+ ((DefaultPlcReadRequest) readRequest).getReader(), curFields));
+ }
+
+ return processedRequests;
+ }
+
+
+}
diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaProtocolLogic.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaProtocolLogic.java
new file mode 100644
index 0000000..2ea9078
--- /dev/null
+++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaProtocolLogic.java
@@ -0,0 +1,905 @@
+/*
+ * 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.PlcRuntimeException;
+import org.apache.plc4x.java.api.messages.*;
+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.api.value.PlcValue;
+import org.apache.plc4x.java.opcua.config.OpcuaConfiguration;
+import org.apache.plc4x.java.opcua.context.SecureChannel;
+import org.apache.plc4x.java.opcua.field.OpcuaField;
+import org.apache.plc4x.java.opcua.readwrite.*;
+import org.apache.plc4x.java.opcua.readwrite.io.*;
+import org.apache.plc4x.java.opcua.readwrite.types.*;
+import org.apache.plc4x.java.spi.ConversationContext;
+import org.apache.plc4x.java.spi.Plc4xProtocolBase;
+import org.apache.plc4x.java.spi.configuration.HasConfiguration;
+import org.apache.plc4x.java.spi.context.DriverContext;
+import org.apache.plc4x.java.spi.generation.*;
+import org.apache.plc4x.java.spi.messages.*;
+import org.apache.plc4x.java.spi.messages.utils.ResponseItem;
+import org.apache.plc4x.java.spi.model.DefaultPlcConsumerRegistration;
+import org.apache.plc4x.java.spi.model.DefaultPlcSubscriptionField;
+import org.apache.plc4x.java.spi.values.IEC61131ValueHandler;
+import org.apache.plc4x.java.spi.values.PlcList;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.math.BigInteger;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+public class OpcuaProtocolLogic extends Plc4xProtocolBase<OpcuaAPU> implements HasConfiguration<OpcuaConfiguration>, PlcSubscriber {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(OpcuaProtocolLogic.class);
+ protected static final PascalString NULL_STRING = new PascalString( "");
+ private static ExpandedNodeId NULL_EXPANDED_NODEID = new ExpandedNodeId(false,
+ false,
+ new NodeIdTwoByte((short) 0),
+ null,
+ null
+ );
+
+ protected static final ExtensionObject NULL_EXTENSION_OBJECT = new ExtensionObject(
+ NULL_EXPANDED_NODEID,
+ new ExtensionObjectEncodingMask(false, false, false),
+ new NullExtension()); // Body
+
+ private static final long EPOCH_OFFSET = 116444736000000000L; //Offset between OPC UA epoch time and linux epoch time.
+ private OpcuaConfiguration configuration;
+ private Map<Long, OpcuaSubscriptionHandle> subscriptions = new HashMap<>();
+ private SecureChannel channel;
+ private AtomicBoolean securedConnection = new AtomicBoolean(false);
+
+ @Override
+ public void setConfiguration(OpcuaConfiguration configuration) {
+ this.configuration = configuration;
+ }
+
+ @Override
+ public void close(ConversationContext<OpcuaAPU> context) {
+ //Nothing
+ }
+
+ @Override
+ public void onDisconnect(ConversationContext<OpcuaAPU> context) {
+ for (Map.Entry<Long, OpcuaSubscriptionHandle> subscriber : subscriptions.entrySet()) {
+ subscriber.getValue().stopSubscriber();
+ }
+ channel.onDisconnect(context);
+ }
+
+ @Override
+ public void setDriverContext(DriverContext driverContext) {
+ super.setDriverContext(driverContext);
+ this.channel = new SecureChannel(driverContext, this.configuration);
+ }
+
+ @Override
+ public void onConnect(ConversationContext<OpcuaAPU> context) {
+ LOGGER.debug("Opcua Driver running in ACTIVE mode.");
+
+ if (this.channel == null) {
+ this.channel = new SecureChannel(driverContext, this.configuration);
+ }
+ this.channel.onConnect(context);
+ }
+
+ @Override
+ public void onDiscover(ConversationContext<OpcuaAPU> context) {
+ // Only the TCP transport supports login.
+ LOGGER.debug("Opcua Driver running in ACTIVE mode, discovering endpoints");
+ channel.onDiscover(context);
+ }
+
+ @Override
+ public CompletableFuture<PlcReadResponse> read(PlcReadRequest readRequest) {
+ LOGGER.trace("Reading Value");
+
+ CompletableFuture<PlcReadResponse> future = new CompletableFuture<>();
+ DefaultPlcReadRequest request = (DefaultPlcReadRequest) readRequest;
+
+ RequestHeader requestHeader = new RequestHeader(channel.getAuthenticationToken(),
+ SecureChannel.getCurrentDateTime(),
+ channel.getRequestHandle(),
+ 0L,
+ NULL_STRING,
+ SecureChannel.REQUEST_TIMEOUT_LONG,
+ NULL_EXTENSION_OBJECT);
+
+ ReadValueId[] readValueArray = new ReadValueId[request.getFieldNames().size()];
+ Iterator<String> iterator = request.getFieldNames().iterator();
+ for (int i = 0; i < request.getFieldNames().size(); i++ ) {
+ String fieldName = iterator.next();
+ OpcuaField field = (OpcuaField) request.getField(fieldName);
+
+ NodeId nodeId = generateNodeId(field);
+
+ readValueArray[i] = new ReadValueId(nodeId,
+ 0xD,
+ NULL_STRING,
+ new QualifiedName(0, NULL_STRING));
+ }
+
+ ReadRequest opcuaReadRequest = new ReadRequest(
+ requestHeader,
+ 0.0d,
+ TimestampsToReturn.timestampsToReturnNeither,
+ readValueArray.length,
+ readValueArray);
+
+ ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
+ false, //Server Index Specified
+ new NodeIdFourByte((short) 0, Integer.valueOf(opcuaReadRequest.getIdentifier())),
+ null,
+ null);
+
+ ExtensionObject extObject = new ExtensionObject(
+ expandedNodeId,
+ null,
+ opcuaReadRequest);
+
+ try {
+ WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), true);
+ ExtensionObjectIO.staticSerialize(buffer, extObject);
+
+ /* Functional Consumer example using inner class */
+ Consumer<byte []> consumer = opcuaResponse -> {
+ PlcReadResponse response = null;
+ try {
+ response = new DefaultPlcReadResponse(request, readResponse(request.getFieldNames(), ((ReadResponse) ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaResponse, true), false).getBody()).getResults()));
+ } catch (ParseException e) {
+ e.printStackTrace();
+ };
+
+ // Pass the response back to the application.
+ future.complete(response);
+ };
+
+ /* Functional Consumer example using inner class */
+ Consumer<TimeoutException> timeout = t -> {
+
+ // Pass the response back to the application.
+ future.completeExceptionally(t);
+ };
+
+ /* Functional Consumer example using inner class */
+ BiConsumer<OpcuaAPU, Throwable> error = (message, t) -> {
+
+ // Pass the response back to the application.
+ future.completeExceptionally(t);
+ };
+
+ channel.submit(context, timeout, error, consumer, buffer);
+
+ } catch (ParseException e) {
+ LOGGER.error("Unable to serialise the ReadRequest");
+ }
+
+ return future;
+ }
+
+ private NodeId generateNodeId(OpcuaField field) {
+ NodeId nodeId = null;
+ if (field.getIdentifierType() == OpcuaIdentifierType.BINARY_IDENTIFIER) {
+ nodeId = new NodeId(new NodeIdTwoByte(Short.valueOf(field.getIdentifier())));
+ } else if (field.getIdentifierType() == OpcuaIdentifierType.NUMBER_IDENTIFIER) {
+ nodeId = new NodeId(new NodeIdNumeric((short) field.getNamespace(), Long.valueOf(field.getIdentifier())));
+ } else if (field.getIdentifierType() == OpcuaIdentifierType.GUID_IDENTIFIER) {
+ UUID guid = UUID.fromString(field.getIdentifier());
+ byte[] guidBytes = new byte[16];
+ System.arraycopy(guid.getMostSignificantBits(), 0, guidBytes, 0, 8);
+ System.arraycopy(guid.getLeastSignificantBits(), 0, guidBytes, 8, 8);
+ nodeId = new NodeId(new NodeIdGuid((short) field.getNamespace(), guidBytes));
+ } else if (field.getIdentifierType() == OpcuaIdentifierType.STRING_IDENTIFIER) {
+ nodeId = new NodeId(new NodeIdString((short) field.getNamespace(), new PascalString(field.getIdentifier())));
+ }
+ return nodeId;
+ }
+
+ public Map<String, ResponseItem<PlcValue>> readResponse(LinkedHashSet<String> fieldNames, DataValue[] results) {
+ PlcResponseCode responseCode = PlcResponseCode.OK;
+ Map<String, ResponseItem<PlcValue>> response = new HashMap<>();
+ int count = 0;
+ for ( String field : fieldNames ) {
+ PlcValue value = null;
+ if (results[count].getValueSpecified()) {
+ Variant variant = results[count].getValue();
+ LOGGER.trace("Response of type {}", variant.getClass().toString());
+ if (variant instanceof VariantBoolean) {
+ byte[] array = ((VariantBoolean) variant).getValue();
+ int length = array.length;
+ Byte[] tmpValue = new Byte[length];
+ for (int i = 0; i < length; i++) {
+ tmpValue[i] = array[i];
+ }
+ value = IEC61131ValueHandler.of(tmpValue);
+ } else if (variant instanceof VariantSByte) {
+ byte[] array = ((VariantSByte) variant).getValue();
+ int length = array.length;
+ Byte[] tmpValue = new Byte[length];
+ for (int i = 0; i < length; i++) {
+ tmpValue[i] = array[i];
+ }
+ value = IEC61131ValueHandler.of(tmpValue);
+ } else if (variant instanceof VariantByte) {
+ short[] array = ((VariantByte) variant).getValue();
+ int length = array.length;
+ Short[] tmpValue = new Short[length];
+ for (int i = 0; i < length; i++) {
+ tmpValue[i] = array[i];
+ }
+ value = IEC61131ValueHandler.of(tmpValue);
+ } else if (variant instanceof VariantInt16) {
+ short[] array = ((VariantInt16) variant).getValue();
+ int length = array.length;
+ Short[] tmpValue = new Short[length];
+ for (int i = 0; i < length; i++) {
+ tmpValue[i] = array[i];
+ }
+ value = IEC61131ValueHandler.of(tmpValue);
+ } else if (variant instanceof VariantUInt16) {
+ int[] array = ((VariantUInt16) variant).getValue();
+ int length = array.length;
+ Integer[] tmpValue = new Integer[length];
+ for (int i = 0; i < length; i++) {
+ tmpValue[i] = array[i];
+ }
+ value = IEC61131ValueHandler.of(tmpValue);
+ } else if (variant instanceof VariantInt32) {
+ int[] array = ((VariantInt32) variant).getValue();
+ int length = array.length;
+ Integer[] tmpValue = new Integer[length];
+ for (int i = 0; i < length; i++) {
+ tmpValue[i] = array[i];
+ }
+ value = IEC61131ValueHandler.of(tmpValue);
+ } else if (variant instanceof VariantUInt32) {
+ long[] array = ((VariantUInt32) variant).getValue();
+ int length = array.length;
+ Long[] tmpValue = new Long[length];
+ for (int i = 0; i < length; i++) {
+ tmpValue[i] = array[i];
+ }
+ value = IEC61131ValueHandler.of(tmpValue);
+ } else if (variant instanceof VariantInt64) {
+ long[] array = ((VariantInt64) variant).getValue();
+ int length = array.length;
+ Long[] tmpValue = new Long[length];
+ for (int i = 0; i < length; i++) {
+ tmpValue[i] = array[i];
+ }
+ value = IEC61131ValueHandler.of(tmpValue);
+ } else if (variant instanceof VariantUInt64) {
+ value = IEC61131ValueHandler.of(((VariantUInt64) variant).getValue());
+ } else if (variant instanceof VariantFloat) {
+ float[] array = ((VariantFloat) variant).getValue();
+ int length = array.length;
+ Float[] tmpValue = new Float[length];
+ for (int i = 0; i < length; i++) {
+ tmpValue[i] = array[i];
+ }
+ value = IEC61131ValueHandler.of(tmpValue);
+ } else if (variant instanceof VariantDouble) {
+ double[] array = ((VariantDouble) variant).getValue();
+ int length = array.length;
+ Double[] tmpValue = new Double[length];
+ for (int i = 0; i < length; i++) {
+ tmpValue[i] = array[i];
+ }
+ value = IEC61131ValueHandler.of(tmpValue);
+ } else if (variant instanceof VariantString) {
+ int length = ((VariantString) variant).getValue().length;
+ PascalString[] stringArray = ((VariantString) variant).getValue();
+ String[] tmpValue = new String[length];
+ for (int i = 0; i < length; i++) {
+ tmpValue[i] = stringArray[i].getStringValue();
+ }
+ value = IEC61131ValueHandler.of(tmpValue);
+ } else if (variant instanceof VariantDateTime) {
+ long[] array = ((VariantDateTime) variant).getValue();
+ int length = array.length;
+ LocalDateTime[] tmpValue = new LocalDateTime[length];
+ for (int i = 0; i < length; i++) {
+ tmpValue[i] = LocalDateTime.ofInstant(Instant.ofEpochMilli(getDateTime(array[i])), ZoneOffset.UTC);
+ }
+ value = IEC61131ValueHandler.of(tmpValue);
+ } else if (variant instanceof VariantGuid) {
+ GuidValue[] array = ((VariantGuid) variant).getValue();
+ int length = array.length;
+ String[] tmpValue = new String[length];
+ for (int i = 0; i < length; i++) {
+ //These two data section aren't little endian like the rest.
+ byte[] data4Bytes = array[i].getData4();
+ int data4 = 0;
+ for (int k = 0; k < data4Bytes.length; k++)
+ {
+ data4 = (data4 << 8) + (data4Bytes[k] & 0xff);
+ }
+ byte[] data5Bytes = array[i].getData5();
+ long data5 = 0;
+ for (int k = 0; k < data5Bytes.length; k++)
+ {
+ data5 = (data5 << 8) + (data5Bytes[k] & 0xff);
+ }
+ tmpValue[i] = Long.toHexString(array[i].getData1()) + "-" + Integer.toHexString(array[i].getData2()) + "-" + Integer.toHexString(array[i].getData3()) + "-" + Integer.toHexString(data4) + "-" + Long.toHexString(data5);
+ }
+ value = IEC61131ValueHandler.of(tmpValue);
+ } else if (variant instanceof VariantXmlElement) {
+ int length = ((VariantXmlElement) variant).getValue().length;
+ PascalString[] stringArray = ((VariantXmlElement) variant).getValue();
+ String[] tmpValue = new String[length];
+ for (int i = 0; i < length; i++) {
+ tmpValue[i] = stringArray[i].getStringValue();
+ }
+ value = IEC61131ValueHandler.of(tmpValue);
+ } else if (variant instanceof VariantLocalizedText) {
+ int length = ((VariantLocalizedText) variant).getValue().length;
+ LocalizedText[] stringArray = ((VariantLocalizedText) variant).getValue();
+ String[] tmpValue = new String[length];
+ for (int i = 0; i < length; i++) {
+ tmpValue[i] = "";
+ tmpValue[i] += stringArray[i].getLocaleSpecified() ? stringArray[i].getLocale().getStringValue() + "|" : "";
+ tmpValue[i] += stringArray[i].getTextSpecified() ? stringArray[i].getText().getStringValue() : "";
+ }
+ value = IEC61131ValueHandler.of(tmpValue);
+ } else if (variant instanceof VariantQualifiedName) {
+ int length = ((VariantQualifiedName) variant).getValue().length;
+ QualifiedName[] stringArray = ((VariantQualifiedName) variant).getValue();
+ String[] tmpValue = new String[length];
+ for (int i = 0; i < length; i++) {
+ tmpValue[i] = "ns=" + stringArray[i].getNamespaceIndex() + ";s=" + stringArray[i].getName().getStringValue();
+ }
+ value = IEC61131ValueHandler.of(tmpValue);
+ } else if (variant instanceof VariantExtensionObject) {
+ int length = ((VariantExtensionObject) variant).getValue().length;
+ ExtensionObject[] stringArray = ((VariantExtensionObject) variant).getValue();
+ String[] tmpValue = new String[length];
+ for (int i = 0; i < length; i++) {
+ tmpValue[i] = stringArray[i].toString();
+ }
+ value = IEC61131ValueHandler.of(tmpValue);
+ } else if (variant instanceof VariantNodeId) {
+ int length = ((VariantNodeId) variant).getValue().length;
+ NodeId[] stringArray = ((VariantNodeId) variant).getValue();
+ String[] tmpValue = new String[length];
+ for (int i = 0; i < length; i++) {
+ tmpValue[i] = stringArray[i].toString();
+ }
+ value = IEC61131ValueHandler.of(tmpValue);
+ }else if (variant instanceof VariantStatusCode) {
+ int length = ((VariantStatusCode) variant).getValue().length;
+ StatusCode[] stringArray = ((VariantStatusCode) variant).getValue();
+ String[] tmpValue = new String[length];
+ for (int i = 0; i < length; i++) {
+ tmpValue[i] = stringArray[i].toString();
+ }
+ value = IEC61131ValueHandler.of(tmpValue);
+ } else if (variant instanceof VariantByteString) {
+ PlcList plcList = new PlcList();
+ ByteStringArray[] array = ((VariantByteString) variant).getValue();
+ for (int k = 0; k < array.length; k++) {
+ int length = array[k].getValue().length;
+ Short[] tmpValue = new Short[length];
+ for (int i = 0; i < length; i++) {
+ tmpValue[i] = array[k].getValue()[i];
+ }
+ plcList.add(IEC61131ValueHandler.of(tmpValue));
+ }
+ value = plcList;
+ } else {
+ responseCode = PlcResponseCode.UNSUPPORTED;
+ LOGGER.error("Data type - " + variant.getClass() + " is not supported ");
+ }
+ } else {
+ if (results[count].getStatusCode().getStatusCode() == OpcuaStatusCode.BadNodeIdUnknown.getValue()) {
+ responseCode = PlcResponseCode.NOT_FOUND;
+ } else {
+ responseCode = PlcResponseCode.UNSUPPORTED;
+ }
+ LOGGER.error("Error while reading value from OPC UA server error code:- " + results[count].getStatusCode().toString());
+ }
+ count++;
+ response.put(field, new ResponseItem<>(responseCode, value));
+ }
+ return response;
+ }
+
+ private Variant fromPlcValue(String fieldName, OpcuaField field, PlcWriteRequest request) {
+ PlcList valueObject;
+ if (request.getPlcValue(fieldName).getObject() instanceof ArrayList) {
+ valueObject = (PlcList) request.getPlcValue(fieldName);
+ } else {
+ ArrayList<PlcValue> list = new ArrayList<>();
+ list.add(request.getPlcValue(fieldName));
+ valueObject = new PlcList(list);
+ }
+
+ List<PlcValue> plcValueList = valueObject.getList();
+ String dataType = field.getPlcDataType();
+ if (dataType.equals("NULL")) {
+ if (plcValueList.get(0).getObject() instanceof Boolean) {
+ dataType = "BOOL";
+ } else if (plcValueList.get(0).getObject() instanceof Byte) {
+ dataType = "SINT";
+ } else if (plcValueList.get(0).getObject() instanceof Short) {
+ dataType = "INT";
+ } else if (plcValueList.get(0).getObject() instanceof Integer) {
+ dataType = "DINT";
+ } else if (plcValueList.get(0).getObject() instanceof Long) {
+ dataType = "LINT";
+ } else if (plcValueList.get(0).getObject() instanceof Float) {
+ dataType = "REAL";
+ } else if (plcValueList.get(0).getObject() instanceof Double) {
+ dataType = "LREAL";
+ } else if (plcValueList.get(0).getObject() instanceof String) {
+ dataType = "STRING";
+ }
+ }
+ int length = valueObject.getLength();
+ switch (dataType) {
+ case "BOOL":
+ case "BIT":
+ byte[] tmpBOOL = new byte[length];
+ for (int i = 0; i < length; i++) {
+ tmpBOOL[i] = valueObject.getIndex(i).getByte();
+ }
+ return new VariantBoolean(length == 1 ? false : true,
+ false,
+ null,
+ null,
+ length == 1 ? null : length,
+ tmpBOOL);
+ case "BYTE":
+ case "BITARR8":
+ case "USINT":
+ case "UINT8":
+ case "BIT8":
+ short[] tmpBYTE = new short[length];
+ for (int i = 0; i < length; i++) {
+ tmpBYTE[i] = valueObject.getIndex(i).getByte();
+ }
+ return new VariantByte(length == 1 ? false : true,
+ false,
+ null,
+ null,
+ length == 1 ? null : length,
+ tmpBYTE);
+ case "SINT":
+ case "INT8":
+ byte[] tmpSINT = new byte[length];
+ for (int i = 0; i < length; i++) {
+ tmpSINT[i] = valueObject.getIndex(i).getByte();
+ }
+ return new VariantSByte(length == 1 ? false : true,
+ false,
+ null,
+ null,
+ length == 1 ? null : length,
+ tmpSINT);
+ case "INT":
+ case "INT16":
+ short[] tmpINT16 = new short[length];
+ for (int i = 0; i < length; i++) {
+ tmpINT16[i] = valueObject.getIndex(i).getShort();
+ }
+ return new VariantInt16(length == 1 ? false : true,
+ false,
+ null,
+ null,
+ length == 1 ? null : length,
+ tmpINT16);
+ case "UINT":
+ case "UINT16":
+ case "WORD":
+ case "BITARR16":
+ int[] tmpUINT = new int[length];
+ for (int i = 0; i < length; i++) {
+ tmpUINT[i] = valueObject.getIndex(i).getInt();
+ }
+ return new VariantUInt16(length == 1 ? false : true,
+ false,
+ null,
+ null,
+ length == 1 ? null : length,
+ tmpUINT);
+ case "DINT":
+ case "INT32":
+ int[] tmpDINT = new int[length];
+ for (int i = 0; i < length; i++) {
+ tmpDINT[i] = valueObject.getIndex(i).getInt();
+ }
+ return new VariantInt32(length == 1 ? false : true,
+ false,
+ null,
+ null,
+ length == 1 ? null : length,
+ tmpDINT);
+ case "UDINT":
+ case "UINT32":
+ case "DWORD":
+ case "BITARR32":
+ long[] tmpUDINT = new long[length];
+ for (int i = 0; i < length; i++) {
+ tmpUDINT[i] = valueObject.getIndex(i).getLong();
+ }
+ return new VariantUInt32(length == 1 ? false : true,
+ false,
+ null,
+ null,
+ length == 1 ? null : length,
+ tmpUDINT);
+ case "LINT":
+ case "INT64":
+ long[] tmpLINT = new long[length];
+ for (int i = 0; i < length; i++) {
+ tmpLINT[i] = valueObject.getIndex(i).getLong();
+ }
+ return new VariantInt64(length == 1 ? false : true,
+ false,
+ null,
+ null,
+ length == 1 ? null : length,
+ tmpLINT);
+ case "ULINT":
+ case "UINT64":
+ case "LWORD":
+ case "BITARR64":
+ BigInteger[] tmpULINT = new BigInteger[length];
+ for (int i = 0; i < length; i++) {
+ tmpULINT[i] = valueObject.getIndex(i).getBigInteger();
+ }
+ return new VariantUInt64(length == 1 ? false : true,
+ false,
+ null,
+ null,
+ length == 1 ? null : length,
+ tmpULINT);
+ case "REAL":
+ case "FLOAT":
+ float[] tmpREAL = new float[length];
+ for (int i = 0; i < length; i++) {
+ tmpREAL[i] = valueObject.getIndex(i).getFloat();
+ }
+ return new VariantFloat(length == 1 ? false : true,
+ false,
+ null,
+ null,
+ length == 1 ? null : length,
+ tmpREAL);
+ case "LREAL":
+ case "DOUBLE":
+ double[] tmpLREAL = new double[length];
+ for (int i = 0; i < length; i++) {
+ tmpLREAL[i] = valueObject.getIndex(i).getDouble();
+ }
+ return new VariantDouble(length == 1 ? false : true,
+ false,
+ null,
+ null,
+ length == 1 ? null : length,
+ tmpLREAL);
+ case "CHAR":
+ case "WCHAR":
+ case "STRING":
+ case "WSTRING":
+ case "STRING16":
+ PascalString[] tmpString = new PascalString[length];
+ for (int i = 0; i < length; i++) {
+ String s = valueObject.getIndex(i).getString();
+ tmpString[i] = new PascalString(s);
+ }
+ return new VariantString(length == 1 ? false : true,
+ false,
+ null,
+ null,
+ length == 1 ? null : length,
+ tmpString);
+ case "DATE_AND_TIME":
+ long[] tmpDateTime = new long[length];
+ for (int i = 0; i < length; i++) {
+ tmpDateTime[i] = valueObject.getIndex(i).getDateTime().toEpochSecond(ZoneOffset.UTC);
+ }
+ return new VariantDateTime(length == 1 ? false : true,
+ false,
+ null,
+ null,
+ length == 1 ? null : length,
+ tmpDateTime);
+ default:
+ throw new PlcRuntimeException("Unsupported write field type " + dataType);
+ }
+ }
+
+ @Override
+ public CompletableFuture<PlcWriteResponse> write(PlcWriteRequest writeRequest) {
+ LOGGER.trace("Writing Value");
+ CompletableFuture<PlcWriteResponse> future = new CompletableFuture<>();
+ DefaultPlcWriteRequest request = (DefaultPlcWriteRequest) writeRequest;
+
+ RequestHeader requestHeader = new RequestHeader(channel.getAuthenticationToken(),
+ SecureChannel.getCurrentDateTime(),
+ channel.getRequestHandle(),
+ 0L,
+ NULL_STRING,
+ SecureChannel.REQUEST_TIMEOUT_LONG,
+ NULL_EXTENSION_OBJECT);
+
+ WriteValue[] writeValueArray = new WriteValue[request.getFieldNames().size()];
+ Iterator<String> iterator = request.getFieldNames().iterator();
+ for (int i = 0; i < request.getFieldNames().size(); i++ ) {
+ String fieldName = iterator.next();
+ OpcuaField field = (OpcuaField) request.getField(fieldName);
+
+ NodeId nodeId = generateNodeId(field);
+
+ writeValueArray[i] = new WriteValue(nodeId,
+ 0xD,
+ NULL_STRING,
+ new DataValue(
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ fromPlcValue(fieldName, field, writeRequest),
+ null,
+ null,
+ null,
+ null,
+ null));
+ }
+
+ WriteRequest opcuaWriteRequest = new WriteRequest(
+ requestHeader,
+ writeValueArray.length,
+ writeValueArray);
+
+ ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
+ false, //Server Index Specified
+ new NodeIdFourByte((short) 0, Integer.valueOf(opcuaWriteRequest.getIdentifier())),
+ null,
+ null);
+
+ ExtensionObject extObject = new ExtensionObject(
+ expandedNodeId,
+ null,
+ opcuaWriteRequest);
+
+ try {
+ WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), true);
+ ExtensionObjectIO.staticSerialize(buffer, extObject);
+
+ /* Functional Consumer example using inner class */
+ Consumer<byte[]> consumer = opcuaResponse -> {
+ WriteResponse responseMessage = null;
+ try {
+ responseMessage = (WriteResponse) ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaResponse, true), false).getBody();
+ } catch (ParseException e) {
+ e.printStackTrace();
+ }
+ PlcWriteResponse response = writeResponse(request, responseMessage);
+
+ // Pass the response back to the application.
+ future.complete(response);
+ };
+
+ /* Functional Consumer example using inner class */
+ Consumer<TimeoutException> timeout = t -> {
+ // Pass the response back to the application.
+ future.completeExceptionally(t);
+ };
+
+ /* Functional Consumer example using inner class */
+ BiConsumer<OpcuaAPU, Throwable> error = (message, t) -> {
+ // Pass the response back to the application.
+ future.completeExceptionally(t);
+ };
+
+ channel.submit(context, timeout, error, consumer, buffer);
+
+ } catch (ParseException e) {
+ LOGGER.error("Unable to serialise the ReadRequest");
+ }
+
+ return future;
+ }
+
+ private PlcWriteResponse writeResponse(DefaultPlcWriteRequest request, WriteResponse writeResponse) {
+ Map<String, PlcResponseCode> responseMap = new HashMap<>();
+ StatusCode[] results = writeResponse.getResults();
+ Iterator<String> responseIterator = request.getFieldNames().iterator();
+ for (int i = 0; i < request.getFieldNames().size(); i++ ) {
+ String fieldName = responseIterator.next();
+ OpcuaStatusCode statusCode = OpcuaStatusCode.enumForValue(results[i].getStatusCode());
+ switch (statusCode) {
+ case Good:
+ responseMap.put(fieldName, PlcResponseCode.OK);
+ break;
+ case BadNodeIdUnknown:
+ responseMap.put(fieldName, PlcResponseCode.NOT_FOUND);
+ break;
+ default:
+ responseMap.put(fieldName, PlcResponseCode.REMOTE_ERROR);
+ }
+ }
+ return new DefaultPlcWriteResponse(request, responseMap);
+ }
+
+
+ @Override
+ public CompletableFuture<PlcSubscriptionResponse> subscribe(PlcSubscriptionRequest subscriptionRequest) {
+ CompletableFuture<PlcSubscriptionResponse> future = CompletableFuture.supplyAsync(() -> {
+ Map<String, ResponseItem<PlcSubscriptionHandle>> values = new HashMap<>();
+ long subscriptionId = -1L;
+ ArrayList<String> fields = new ArrayList<>( subscriptionRequest.getFieldNames() );
+ long cycleTime = ((DefaultPlcSubscriptionField) subscriptionRequest.getField(fields.get(0))).getDuration().orElse(Duration.ofMillis(1000)).toMillis();
+
+ try {
+ CompletableFuture<CreateSubscriptionResponse> subscription = onSubscribeCreateSubscription(cycleTime);
+ CreateSubscriptionResponse response = subscription.get(SecureChannel.REQUEST_TIMEOUT_LONG, TimeUnit.MILLISECONDS);
+ subscriptionId = response.getSubscriptionId();
+ subscriptions.put(subscriptionId, new OpcuaSubscriptionHandle(context, this, channel, subscriptionRequest, subscriptionId, cycleTime));
+ } catch (Exception e) {
+ throw new PlcRuntimeException("Unable to subscribe because of: " + e.getMessage());
+ }
+
+ for (String fieldName : subscriptionRequest.getFieldNames()) {
+ final DefaultPlcSubscriptionField fieldDefaultPlcSubscription = (DefaultPlcSubscriptionField) subscriptionRequest.getField(fieldName);
+ if (!(fieldDefaultPlcSubscription.getPlcField() instanceof OpcuaField)) {
+ values.put(fieldName, new ResponseItem<>(PlcResponseCode.INVALID_ADDRESS, null));
+ } else {
+ values.put(fieldName, new ResponseItem<>(PlcResponseCode.OK, subscriptions.get(subscriptionId)));
+ }
+ }
+ return new DefaultPlcSubscriptionResponse(subscriptionRequest, values);
+ });
+
+ return future;
+ }
+
+ private CompletableFuture<CreateSubscriptionResponse> onSubscribeCreateSubscription(long cycleTime) {
+ CompletableFuture<CreateSubscriptionResponse> future = new CompletableFuture<>();
+ LOGGER.trace("Entering creating subscription request");
+
+ RequestHeader requestHeader = new RequestHeader(channel.getAuthenticationToken(),
+ SecureChannel.getCurrentDateTime(),
+ channel.getRequestHandle(),
+ 0L,
+ NULL_STRING,
+ SecureChannel.REQUEST_TIMEOUT_LONG,
+ NULL_EXTENSION_OBJECT);
+
+ CreateSubscriptionRequest createSubscriptionRequest = new CreateSubscriptionRequest(
+ requestHeader,
+ cycleTime,
+ 12000,
+ 5,
+ 65536,
+ true,
+ (short) 0
+ );
+
+ ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
+ false, //Server Index Specified
+ new NodeIdFourByte((short) 0, Integer.valueOf(createSubscriptionRequest.getIdentifier())),
+ null,
+ null);
+
+ ExtensionObject extObject = new ExtensionObject(
+ expandedNodeId,
+ null,
+ createSubscriptionRequest);
+
+ try {
+ WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), true);
+ ExtensionObjectIO.staticSerialize(buffer, extObject);
+
+ /* Functional Consumer example using inner class */
+ Consumer<byte[]> consumer = opcuaResponse -> {
+ CreateSubscriptionResponse responseMessage = null;
+ try {
+ responseMessage = (CreateSubscriptionResponse) ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaResponse, true), false).getBody();
+ } catch (ParseException e) {
+ e.printStackTrace();
+ }
+
+ // Pass the response back to the application.
+ future.complete(responseMessage);
+
+ };
+
+ /* Functional Consumer example using inner class */
+ Consumer<TimeoutException> timeout = e -> {
+ LOGGER.error("Timeout while waiting on the crate subscription response");
+ e.printStackTrace();
+ // Pass the response back to the application.
+ future.completeExceptionally(e);
+ };
+
+ /* Functional Consumer example using inner class */
+ BiConsumer<OpcuaAPU, Throwable> error = (message, e) -> {
+ LOGGER.error("Error while creating the subscription");
+ e.printStackTrace();
+ // Pass the response back to the application.
+ future.completeExceptionally(e);
+ };
+
+ channel.submit(context, timeout, error, consumer, buffer);
+ } catch (ParseException e) {
+ LOGGER.error("Error while creating the subscription");
+ e.printStackTrace();
+ future.completeExceptionally(e);
+ }
+ return future;
+ }
+
+ @Override
+ public CompletableFuture<PlcUnsubscriptionResponse> unsubscribe(PlcUnsubscriptionRequest unsubscriptionRequest) {
+ unsubscriptionRequest.getSubscriptionHandles().forEach(o -> {
+ OpcuaSubscriptionHandle opcuaSubHandle = (OpcuaSubscriptionHandle) o;
+ opcuaSubHandle.stopSubscriber();
+ });
+ return null;
+ }
+
+ public void removeSubscription(Long subscriptionId) {
+ subscriptions.remove(subscriptionId);
+ }
+
+ @Override
+ public PlcConsumerRegistration register(Consumer<PlcSubscriptionEvent> consumer, Collection<PlcSubscriptionHandle> handles) {
+ List<PlcConsumerRegistration> registrations = new LinkedList<>();
+ // Register the current consumer for each of the given subscription handles
+ for (PlcSubscriptionHandle subscriptionHandle : handles) {
+ LOGGER.debug("Registering Consumer");
+ final PlcConsumerRegistration consumerRegistration = subscriptionHandle.register(consumer);
+ registrations.add(consumerRegistration);
+ }
+ return new DefaultPlcConsumerRegistration((PlcSubscriber) this, consumer, handles.toArray(new PlcSubscriptionHandle[0]));
+ }
+
+ @Override
+ public void unregister(PlcConsumerRegistration registration) {
+ registration.unregister();
+ }
+
+ public static long getDateTime(long dateTime) {
+ return (dateTime - EPOCH_OFFSET) / 10000;
+ }
+
+ private GuidValue toGuidValue(String identifier) {
+ LOGGER.error("Querying Guid nodes is not supported");
+ byte[] data4 = new byte[] {0,0};
+ byte[] data5 = new byte[] {0,0,0,0,0,0};
+ return new GuidValue(0L,0,0,data4, data5);
+ }
+}
diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandle.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandle.java
new file mode 100644
index 0000000..4ad90bf
--- /dev/null
+++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandle.java
@@ -0,0 +1,489 @@
+/*
+ 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.messages.PlcSubscriptionEvent;
+import org.apache.plc4x.java.api.messages.PlcSubscriptionRequest;
+import org.apache.plc4x.java.api.model.PlcConsumerRegistration;
+import org.apache.plc4x.java.api.value.PlcValue;
+import org.apache.plc4x.java.opcua.context.SecureChannel;
+import org.apache.plc4x.java.opcua.field.OpcuaField;
+import org.apache.plc4x.java.opcua.readwrite.*;
+import org.apache.plc4x.java.opcua.readwrite.io.ExtensionObjectIO;
+import org.apache.plc4x.java.opcua.readwrite.types.*;
+import org.apache.plc4x.java.spi.ConversationContext;
+import org.apache.plc4x.java.spi.generation.*;
+import org.apache.plc4x.java.spi.messages.DefaultPlcSubscriptionEvent;
+import org.apache.plc4x.java.spi.messages.utils.ResponseItem;
+import org.apache.plc4x.java.spi.model.DefaultPlcConsumerRegistration;
+import org.apache.plc4x.java.spi.model.DefaultPlcSubscriptionField;
+import org.apache.plc4x.java.spi.model.DefaultPlcSubscriptionHandle;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.time.Instant;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+public class OpcuaSubscriptionHandle extends DefaultPlcSubscriptionHandle {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(OpcuaSubscriptionHandle.class);
+
+ private Set<Consumer<PlcSubscriptionEvent>> consumers;
+ private List<String> fieldNames;
+ private SecureChannel channel;
+ private PlcSubscriptionRequest subscriptionRequest;
+ private AtomicBoolean destroy = new AtomicBoolean(false);
+ private OpcuaProtocolLogic plcSubscriber;
+ private Long subscriptionId;
+ private long cycleTime;
+ private long revisedCycleTime;
+ private boolean complete = false;
+
+ private final AtomicLong clientHandles = new AtomicLong(1L);
+
+ private ConversationContext context;
+
+ public OpcuaSubscriptionHandle(ConversationContext<OpcuaAPU> context, OpcuaProtocolLogic plcSubscriber, SecureChannel channel, PlcSubscriptionRequest subscriptionRequest, Long subscriptionId, long cycleTime) {
+ super(plcSubscriber);
+ this.consumers = new HashSet<>();
+ this.subscriptionRequest = subscriptionRequest;
+ this.fieldNames = new ArrayList<>( subscriptionRequest.getFieldNames() );
+ this.channel = channel;
+ this.subscriptionId = subscriptionId;
+ this.plcSubscriber = plcSubscriber;
+ this.cycleTime = cycleTime;
+ this.revisedCycleTime = cycleTime;
+ this.context = context;
+ try {
+ onSubscribeCreateMonitoredItemsRequest().get();
+ } catch (Exception e) {
+ LOGGER.info("Unable to serialize the Create Monitored Item Subscription Message");
+ e.printStackTrace();
+ plcSubscriber.onDisconnect(context);
+ }
+ startSubscriber();
+ }
+
+ private CompletableFuture<CreateMonitoredItemsResponse> onSubscribeCreateMonitoredItemsRequest() {
+ MonitoredItemCreateRequest[] requestList = new MonitoredItemCreateRequest[this.fieldNames.size()];
+ for (int i = 0; i < this.fieldNames.size(); i++) {
+ final DefaultPlcSubscriptionField fieldDefaultPlcSubscription = (DefaultPlcSubscriptionField) subscriptionRequest.getField(fieldNames.get(i));
+
+ NodeId idNode = generateNodeId((OpcuaField) fieldDefaultPlcSubscription.getPlcField());
+
+ ReadValueId readValueId = new ReadValueId(
+ idNode,
+ 0xD,
+ OpcuaProtocolLogic.NULL_STRING,
+ new QualifiedName(0, OpcuaProtocolLogic.NULL_STRING));
+
+ MonitoringMode monitoringMode;
+ switch (fieldDefaultPlcSubscription.getPlcSubscriptionType()) {
+ case CYCLIC:
+ monitoringMode = MonitoringMode.monitoringModeSampling;
+ break;
+ case CHANGE_OF_STATE:
+ monitoringMode = MonitoringMode.monitoringModeReporting;
+ break;
+ case EVENT:
+ monitoringMode = MonitoringMode.monitoringModeReporting;
+ break;
+ default:
+ monitoringMode = MonitoringMode.monitoringModeReporting;
+ }
+
+ long clientHandle = clientHandles.getAndIncrement();
+
+ MonitoringParameters parameters = new MonitoringParameters(
+ clientHandle,
+ (double) cycleTime, // sampling interval
+ OpcuaProtocolLogic.NULL_EXTENSION_OBJECT, // filter, null means use default
+ 1L, // queue size
+ true // discard oldest
+ );
+
+ MonitoredItemCreateRequest request = new MonitoredItemCreateRequest(
+ readValueId, monitoringMode, parameters);
+
+ requestList[i] = request;
+ }
+
+ CompletableFuture<CreateMonitoredItemsResponse> future = new CompletableFuture<>();
+
+ RequestHeader requestHeader = new RequestHeader(channel.getAuthenticationToken(),
+ SecureChannel.getCurrentDateTime(),
+ channel.getRequestHandle(),
+ 0L,
+ OpcuaProtocolLogic.NULL_STRING,
+ SecureChannel.REQUEST_TIMEOUT_LONG,
+ OpcuaProtocolLogic.NULL_EXTENSION_OBJECT);
+
+ CreateMonitoredItemsRequest createMonitoredItemsRequest = new CreateMonitoredItemsRequest(
+ requestHeader,
+ subscriptionId,
+ TimestampsToReturn.timestampsToReturnNeither,
+ requestList.length,
+ requestList
+ );
+
+ ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
+ false, //Server Index Specified
+ new NodeIdFourByte((short) 0, Integer.valueOf(createMonitoredItemsRequest.getIdentifier())),
+ null,
+ null);
+
+ ExtensionObject extObject = new ExtensionObject(
+ expandedNodeId,
+ null,
+ createMonitoredItemsRequest);
+
+ try {
+ WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), true);
+ ExtensionObjectIO.staticSerialize(buffer, extObject);
+
+ Consumer<byte[]> consumer = opcuaResponse -> {
+ CreateMonitoredItemsResponse responseMessage = null;
+ try {
+ ExtensionObjectDefinition unknownExtensionObject = ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaResponse, true), false).getBody();
+ if (unknownExtensionObject instanceof CreateMonitoredItemsResponse) {
+ responseMessage = (CreateMonitoredItemsResponse) unknownExtensionObject;
+ } else {
+ ServiceFault serviceFault = (ServiceFault) unknownExtensionObject;
+ ResponseHeader header = (ResponseHeader) serviceFault.getResponseHeader();
+ LOGGER.error("Subscription ServiceFault returned from server with error code, '{}'", header.getServiceResult().toString());
+ plcSubscriber.onDisconnect(context);
+ }
+ } catch (ParseException e) {
+ LOGGER.error("Unable to parse the returned Subscription response");
+ e.printStackTrace();
+ plcSubscriber.onDisconnect(context);
+ }
+ for (MonitoredItemCreateResult result : Arrays.stream(responseMessage.getResults()).toArray(MonitoredItemCreateResult[]::new)) {
+ if (OpcuaStatusCode.enumForValue(result.getStatusCode().getStatusCode()) != OpcuaStatusCode.Good) {
+ LOGGER.error("Invalid Field {}, subscription created without this field", fieldNames.get((int) result.getMonitoredItemId()));
+ } else {
+ LOGGER.debug("Field {} was added to the subscription", fieldNames.get((int) result.getMonitoredItemId() - 1));
+ }
+ }
+ future.complete(responseMessage);
+ };
+
+ Consumer<TimeoutException> timeout = e -> {
+ LOGGER.info("Timeout while sending the Create Monitored Item Subscription Message");
+ e.printStackTrace();
+ plcSubscriber.onDisconnect(context);
+ };
+
+ BiConsumer<OpcuaAPU, Throwable> error = (message, e) -> {
+ LOGGER.info("Error while sending the Create Monitored Item Subscription Message");
+ e.printStackTrace();
+ plcSubscriber.onDisconnect(context);
+ };
+
+ channel.submit(context, timeout, error, consumer, buffer);
+
+ } catch (ParseException e) {
+ LOGGER.info("Unable to serialize the Create Monitored Item Subscription Message");
+ e.printStackTrace();
+ plcSubscriber.onDisconnect(context);
+ }
+ return future;
+ }
+
+ private void sleep(long length) {
+ try {
+ Thread.sleep(length);
+ } catch (InterruptedException e) {
+ LOGGER.trace("Interrupted Exception");
+ }
+ }
+
+ /**
+ * Main subscriber loop. For subscription we still need to send a request the server on every cycle.
+ * Which includes a request for an update of the previsouly agreed upon list of tags.
+ * The server will respond at most once every cycle.
+ * @return
+ */
+ public void startSubscriber() {
+ LOGGER.trace("Starting Subscription");
+ CompletableFuture.supplyAsync(() -> {
+ try {
+ LinkedList<SubscriptionAcknowledgement> outstandingAcknowledgements = new LinkedList<>();
+ LinkedList<Long> outstandingRequests = new LinkedList<>();
+ while (!this.destroy.get()) {
+ long requestHandle = channel.getRequestHandle();
+
+ //If we are waiting on a response and haven't received one, just wait until we do. A keep alive will be sent out eventually
+ if (outstandingRequests.size() <= 1) {
+ RequestHeader requestHeader = new RequestHeader(channel.getAuthenticationToken(),
+ SecureChannel.getCurrentDateTime(),
+ requestHandle,
+ 0L,
+ OpcuaProtocolLogic.NULL_STRING,
+ this.revisedCycleTime * 10,
+ OpcuaProtocolLogic.NULL_EXTENSION_OBJECT);
+
+ //Make a copy of the outstanding requests so it isn't modified while we are putting the ack list together.
+ LinkedList<Long> outstandingAcknowledgementsSnapshot = (LinkedList<Long>) outstandingAcknowledgements.clone();
+ SubscriptionAcknowledgement[] acks = new SubscriptionAcknowledgement[outstandingAcknowledgementsSnapshot.size()];
+ ;
+ outstandingAcknowledgementsSnapshot.toArray(acks);
+ int ackLength = outstandingAcknowledgementsSnapshot.size() == 0 ? -1 : outstandingAcknowledgementsSnapshot.size();
+ outstandingAcknowledgements.removeAll(outstandingAcknowledgementsSnapshot);
+
+ PublishRequest publishRequest = new PublishRequest(
+ requestHeader,
+ ackLength,
+ acks
+ );
+
+ ExpandedNodeId extExpandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
+ false, //Server Index Specified
+ new NodeIdFourByte((short) 0, Integer.valueOf(publishRequest.getIdentifier())),
+ null,
+ null);
+
+ ExtensionObject extObject = new ExtensionObject(
+ extExpandedNodeId,
+ null,
+ publishRequest);
+
+ try {
+ WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), true);
+ ExtensionObjectIO.staticSerialize(buffer, extObject);
+
+ /* Create Consumer for the response message, error and timeout to be sent to the Secure Channel */
+ Consumer<byte[]> consumer = opcuaResponse -> {
+ PublishResponse responseMessage = null;
+ ServiceFault serviceFault = null;
+ try {
+ ExtensionObjectDefinition unknownExtensionObject = ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaResponse, true), false).getBody();
+ if (unknownExtensionObject instanceof PublishResponse) {
+ responseMessage = (PublishResponse) unknownExtensionObject;
+ } else {
+ serviceFault = (ServiceFault) unknownExtensionObject;
+ ResponseHeader header = (ResponseHeader) serviceFault.getResponseHeader();
+ LOGGER.error("Subscription ServiceFault returned from server with error code, '{}'", header.getServiceResult().toString());
+ //plcSubscriber.onDisconnect(context);
+ }
+ } catch (ParseException e) {
+ LOGGER.error("Unable to parse the returned Subscription response");
+ e.printStackTrace();
+ plcSubscriber.onDisconnect(context);
+ }
+ if (serviceFault == null) {
+ outstandingRequests.remove(((ResponseHeader) responseMessage.getResponseHeader()).getRequestHandle());
+
+ for (long availableSequenceNumber : responseMessage.getAvailableSequenceNumbers()) {
+ outstandingAcknowledgements.add(new SubscriptionAcknowledgement(this.subscriptionId, availableSequenceNumber));
+ }
+
+ for (ExtensionObject notificationMessage : ((NotificationMessage) responseMessage.getNotificationMessage()).getNotificationData()) {
+ ExtensionObjectDefinition notification = notificationMessage.getBody();
+ if (notification instanceof DataChangeNotification) {
+ LOGGER.trace("Found a Data Change notification");
+ ExtensionObjectDefinition[] items = ((DataChangeNotification) notification).getMonitoredItems();
+ onSubscriptionValue(Arrays.stream(items).toArray(MonitoredItemNotification[]::new));
+ } else {
+ LOGGER.warn("Unsupported Notification type");
+ }
+ }
+ }
+ };
+
+ Consumer<TimeoutException> timeout = e -> {
+ LOGGER.error("Timeout while waiting for subscription response");
+ e.printStackTrace();
+ plcSubscriber.onDisconnect(context);
+ };
+
+ BiConsumer<OpcuaAPU, Throwable> error = (message, e) -> {
+ LOGGER.error("Error while waiting for subscription response");
+ e.printStackTrace();
+ plcSubscriber.onDisconnect(context);
+ };
+
+ outstandingRequests.add(requestHandle);
+ channel.submit(context, timeout, error, consumer, buffer);
+
+ } catch (ParseException e) {
+ LOGGER.warn("Unable to serialize subscription request");
+ e.printStackTrace();
+ }
+ }
+ /* Put the subscriber loop to sleep for the rest of the cycle. */
+ sleep(this.revisedCycleTime);
+ }
+ //Wait for any outstanding responses to arrive, using the request timeout length
+ //sleep(this.revisedCycleTime * 10);
+ complete = true;
+ } catch (Exception e) {
+ LOGGER.error("Failed :(");
+ e.printStackTrace();
+ }
+ return null;
+ });
+ return;
+ }
+
+
+ /**
+ * Stop the subscriber either on disconnect or on error
+ * @return
+ */
+ public void stopSubscriber() {
+ this.destroy.set(true);
+
+ long requestHandle = channel.getRequestHandle();
+
+ RequestHeader requestHeader = new RequestHeader(channel.getAuthenticationToken(),
+ SecureChannel.getCurrentDateTime(),
+ requestHandle,
+ 0L,
+ OpcuaProtocolLogic.NULL_STRING,
+ this.revisedCycleTime * 10,
+ OpcuaProtocolLogic.NULL_EXTENSION_OBJECT);
+
+ long[] subscriptions = new long[1];
+ subscriptions[0] = subscriptionId;
+ DeleteSubscriptionsRequest deleteSubscriptionrequest = new DeleteSubscriptionsRequest(requestHeader,
+ 1,
+ subscriptions
+ );
+
+ ExpandedNodeId extExpandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
+ false, //Server Index Specified
+ new NodeIdFourByte((short) 0, Integer.valueOf(deleteSubscriptionrequest.getIdentifier())),
+ null,
+ null);
+
+ ExtensionObject extObject = new ExtensionObject(
+ extExpandedNodeId,
+ null,
+ deleteSubscriptionrequest);
+
+ try {
+ WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), true);
+ ExtensionObjectIO.staticSerialize(buffer, extObject);
+
+ // Create Consumer for the response message, error and timeout to be sent to the Secure Channel
+ Consumer<byte[]> consumer = opcuaResponse -> {
+ DeleteSubscriptionsResponse responseMessage = null;
+ try {
+ ExtensionObjectDefinition unknownExtensionObject = ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaResponse, true), false).getBody();
+ if (unknownExtensionObject instanceof DeleteSubscriptionsResponse) {
+ responseMessage = (DeleteSubscriptionsResponse) unknownExtensionObject;
+ } else {
+ ServiceFault serviceFault = (ServiceFault) unknownExtensionObject;
+ ResponseHeader header = (ResponseHeader) serviceFault.getResponseHeader();
+ LOGGER.error("Fault when deleteing Subscription ServiceFault return from server with error code, '{}'", header.getServiceResult().toString());
+ }
+ } catch (ParseException e) {
+ LOGGER.error("Unable to parse the returned Delete Subscriptions Response");
+ e.printStackTrace();
+ }
+ };
+
+ Consumer<TimeoutException> timeout = e -> {
+ LOGGER.error("Timeout while waiting for delete subscription response");
+ e.printStackTrace();
+ plcSubscriber.onDisconnect(context);
+ };
+
+ BiConsumer<OpcuaAPU, Throwable> error = (message, e) -> {
+ LOGGER.error("Error while waiting for delete subscription response");
+ e.printStackTrace();
+ plcSubscriber.onDisconnect(context);
+ };
+
+ channel.submit(context, timeout, error, consumer, buffer);
+ } catch (ParseException e) {
+ LOGGER.warn("Unable to serialize subscription request");
+ e.printStackTrace();
+ }
+
+ sleep(500);
+ plcSubscriber.removeSubscription(subscriptionId);
+ }
+
+ /**
+ * Receive the returned values from the OPCUA server and format it so that it can be received by the PLC4X client.
+ * @param values - array of data values to be sent to the client.
+ */
+ private void onSubscriptionValue(MonitoredItemNotification[] values) {
+ LinkedHashSet<String> fieldList = new LinkedHashSet<>();
+ DataValue[] dataValues = new DataValue[values.length];
+ int i = 0;
+ for (MonitoredItemNotification value : values) {
+ fieldList.add(fieldNames.get((int) value.getClientHandle() - 1));
+ dataValues[i] = value.getValue();
+ i++;
+ }
+ Map<String, ResponseItem<PlcValue>> fields = plcSubscriber.readResponse(fieldList, dataValues);
+ final PlcSubscriptionEvent event = new DefaultPlcSubscriptionEvent(Instant.now(), fields);
+
+ consumers.forEach(plcSubscriptionEventConsumer -> {
+ plcSubscriptionEventConsumer.accept(event);
+ });
+ }
+
+ /**
+ * Registers a new Consumer, this allows multiple PLC4X consumers to use the same subscription.
+ * @param consumer - Consumer to be used to send any returned values.
+ * @return PlcConsumerRegistration - return the important information back to the client.
+ */
+ @Override
+ public PlcConsumerRegistration register(Consumer<PlcSubscriptionEvent> consumer) {
+ LOGGER.info("Registering a new OPCUA subscription consumer");
+ consumers.add(consumer);
+ return new DefaultPlcConsumerRegistration(plcSubscriber, consumer, this);
+ }
+
+ /**
+ * Given an PLC4X OpcuaField generate the OPC UA Node Id
+ * @param field - The PLC4X OpcuaField, this is the field generated from the OpcuaField class from the parsed field string.
+ * @return NodeId - Returns an OPC UA Node Id which can be sent over the wire.
+ */
+ private NodeId generateNodeId(OpcuaField field) {
+ NodeId nodeId = null;
+ if (field.getIdentifierType() == OpcuaIdentifierType.BINARY_IDENTIFIER) {
+ nodeId = new NodeId(new NodeIdTwoByte(Short.valueOf(field.getIdentifier())));
+ } else if (field.getIdentifierType() == OpcuaIdentifierType.NUMBER_IDENTIFIER) {
+ nodeId = new NodeId(new NodeIdNumeric((short) field.getNamespace(), Long.valueOf(field.getIdentifier())));
+ } else if (field.getIdentifierType() == OpcuaIdentifierType.GUID_IDENTIFIER) {
+ UUID guid = UUID.fromString(field.getIdentifier());
+ byte[] guidBytes = new byte[16];
+ System.arraycopy(guid.getMostSignificantBits(), 0, guidBytes, 0, 8);
+ System.arraycopy(guid.getLeastSignificantBits(), 0, guidBytes, 8, 8);
+ nodeId = new NodeId(new NodeIdGuid((short) field.getNamespace(), guidBytes));
+ } else if (field.getIdentifierType() == OpcuaIdentifierType.STRING_IDENTIFIER) {
+ nodeId = new NodeId(new NodeIdString((short) field.getNamespace(), new PascalString(field.getIdentifier())));
+ }
+ return nodeId;
+ }
+
+
+
+}
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
deleted file mode 100644
index 756f0cd..0000000
--- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubsriptionHandle.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- 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.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.api.value.PlcValue;
-import org.apache.plc4x.java.spi.messages.DefaultPlcSubscriptionEvent;
-import org.apache.plc4x.java.opcua.connection.OpcuaTcpPlcConnection;
-import org.apache.plc4x.java.spi.messages.utils.ResponseItem;
-import org.apache.plc4x.java.spi.model.DefaultPlcConsumerRegistration;
-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;
-
-/**
- */
-public class OpcuaSubsriptionHandle implements PlcSubscriptionHandle {
-
- private Set<Consumer<PlcSubscriptionEvent>> consumers = new HashSet<>();
- private String fieldName;
- private UInteger clientHandle;
-
- /**
- * @param fieldName corresponding map key in the PLC4X request/reply map
- * @param clientHandle
- */
- public OpcuaSubsriptionHandle(String fieldName, UInteger clientHandle) {
- this.fieldName = fieldName;
- this.clientHandle = clientHandle;
- }
-
- public UInteger getClientHandle() {
- return clientHandle;
- }
-
- /**
- * @param item
- * @param value
- */
- public void onSubscriptionValue(UaMonitoredItem item, DataValue value) {
- consumers.forEach(plcSubscriptionEventConsumer -> {
- PlcResponseCode resultCode = PlcResponseCode.OK;
- PlcValue stringItem = null;
- if (value.getStatusCode() != StatusCode.GOOD) {
- resultCode = PlcResponseCode.NOT_FOUND;
- } else {
- stringItem = OpcuaTcpPlcConnection.encodePlcValue(value);
-
- }
- Map<String, ResponseItem<PlcValue>> fields = new HashMap<>();
- ResponseItem<PlcValue> newPair = new ResponseItem<>(resultCode, stringItem);
- fields.put(fieldName, newPair);
- PlcSubscriptionEvent event = new DefaultPlcSubscriptionEvent(Instant.now(), fields);
- plcSubscriptionEventConsumer.accept(event);
- });
- }
-
- @Override
- public PlcConsumerRegistration register(Consumer<PlcSubscriptionEvent> consumer) {
- consumers.add(consumer);
- return null;
-// return () -> consumers.remove(consumer);
- }
-
-}
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
index d635874..4e12e2d 100644
--- 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
@@ -19,15 +19,19 @@
package org.apache.plc4x.java.opcua;
import org.apache.plc4x.java.PlcDriverManager;
+import org.apache.plc4x.java.api.PlcConnection;
import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
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.PlcResponseCode;
import org.apache.plc4x.java.api.types.PlcSubscriptionType;
-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 org.apache.plc4x.java.opcua.field.OpcuaField;
+import org.apache.plc4x.java.opcua.field.OpcuaPlcFieldHandler;
+import org.apache.plc4x.java.opcua.protocol.OpcuaProtocolLogic;
+import org.apache.plc4x.java.opcua.protocol.OpcuaSubscriptionHandle;
+import org.apache.plc4x.java.spi.connection.DefaultNettyPlcConnection;
import org.apache.plc4x.java.spi.messages.DefaultPlcSubscriptionRequest;
import org.apache.plc4x.java.spi.model.DefaultPlcSubscriptionField;
import org.eclipse.milo.examples.server.ExampleServer;
@@ -93,21 +97,16 @@
} catch (Exception e) {
throw new PlcRuntimeException(e);
}
- OpcuaTcpPlcConnection opcuaConnection = null;
+ PlcConnection opcuaConnection = null;
OpcuaPlcFieldHandler fieldH = new OpcuaPlcFieldHandler();
PlcField field = fieldH.createField(BOOL_IDENTIFIER);
try {
- opcuaConnection = (OpcuaTcpPlcConnection)
- new PlcDriverManager().getConnection("opcua:tcp://127.0.0.1:12686/milo?discovery=false");
+ opcuaConnection = new PlcDriverManager().getConnection("opcua:tcp://127.0.0.1:12686/milo?discovery=false");
} catch (PlcConnectionException e) {
throw new PlcRuntimeException(e);
}
try {
- String[] array = new String[2];
- System.out.printf("%s:%s", array);
-
-
PlcReadRequest.Builder builder = opcuaConnection.readRequestBuilder();
builder.addItem("Bool", BOOL_IDENTIFIER);
builder.addItem("ByteString", BYTE_STRING_IDENTIFIER);
@@ -144,7 +143,9 @@
builder.addItem("DoesNotExists", DOES_NOT_EXIST_IDENTIFIER);
PlcReadRequest request = builder.build();
- PlcReadResponse response = opcuaConnection.read(request).get();
+
+
+ PlcReadResponse response = request.execute().get();
//Collection coll = response.getAllStrings("String");
@@ -165,20 +166,59 @@
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();
+ PlcWriteResponse wResponse = writeRequest.execute().get();
- PlcSubscriptionResponse subResp = opcuaConnection.subscribe(new DefaultPlcSubscriptionRequest(
- opcuaConnection,
- new LinkedHashMap<>(
- Collections.singletonMap("field1",
- new DefaultPlcSubscriptionField(PlcSubscriptionType.CHANGE_OF_STATE, OpcuaField.of(STRING_IDENTIFIER), Duration.of(1, ChronoUnit.SECONDS)))
- )
- )).get();
+ // Create Subscription
+ PlcSubscriptionRequest.Builder sBuilder = opcuaConnection.subscriptionRequestBuilder();
+ sBuilder.addChangeOfStateField("Bool", BOOL_IDENTIFIER);
+ sBuilder.addChangeOfStateField("ByteString", BYTE_STRING_IDENTIFIER);
+ sBuilder.addChangeOfStateField("Byte", BYTE_IDENTIFIER);
+ sBuilder.addChangeOfStateField("Double", DOUBLE_IDENTIFIER);
+ sBuilder.addChangeOfStateField("Float", FLOAT_IDENTIFIER);
+ sBuilder.addChangeOfStateField("Int16", INT16_IDENTIFIER);
+ sBuilder.addChangeOfStateField("Int32", INT32_IDENTIFIER);
+ sBuilder.addChangeOfStateField("Int64", INT64_IDENTIFIER);
+ sBuilder.addChangeOfStateField("Integer", INTEGER_IDENTIFIER);
+ sBuilder.addChangeOfStateField("SByte", SBYTE_IDENTIFIER);
+ sBuilder.addChangeOfStateField("String", STRING_IDENTIFIER);
+ sBuilder.addChangeOfStateField("UInt16", UINT16_IDENTIFIER);
+ sBuilder.addChangeOfStateField("UInt32", UINT32_IDENTIFIER);
+ sBuilder.addChangeOfStateField("UInt64", UINT64_IDENTIFIER);
+ sBuilder.addChangeOfStateField("UInteger", UINTEGER_IDENTIFIER);
- Consumer<PlcSubscriptionEvent> consumer = plcSubscriptionEvent -> System.out.println(plcSubscriptionEvent.toString() + "########################################################################################################################################################################");
- PlcConsumerRegistration registration = opcuaConnection.register(consumer, subResp.getSubscriptionHandles());
- Thread.sleep(7000);
- registration.unregister();
+ sBuilder.addChangeOfStateField("BoolArray", BOOL_ARRAY_IDENTIFIER);
+ sBuilder.addChangeOfStateField("ByteStringArray", BYTE_STRING_ARRAY_IDENTIFIER);
+ sBuilder.addChangeOfStateField("ByteArray", BYTE_ARRAY_IDENTIFIER);
+ sBuilder.addChangeOfStateField("DoubleArray", DOUBLE_ARRAY_IDENTIFIER);
+ sBuilder.addChangeOfStateField("FloatArray", FLOAT_ARRAY_IDENTIFIER);
+ sBuilder.addChangeOfStateField("Int16Array", INT16_ARRAY_IDENTIFIER);
+ sBuilder.addChangeOfStateField("Int32Array", INT32_ARRAY_IDENTIFIER);
+ sBuilder.addChangeOfStateField("Int64Array", INT64_ARRAY_IDENTIFIER);
+ sBuilder.addChangeOfStateField("IntegerArray", INTEGER_ARRAY_IDENTIFIER);
+ sBuilder.addChangeOfStateField("SByteArray", SBYTE_ARRAY_IDENTIFIER);
+ sBuilder.addChangeOfStateField("StringArray", STRING_ARRAY_IDENTIFIER);
+ sBuilder.addChangeOfStateField("UInt16Array", UINT16_ARRAY_IDENTIFIER);
+ sBuilder.addChangeOfStateField("UInt32Array", UINT32_ARRAY_IDENTIFIER);
+ sBuilder.addChangeOfStateField("UInt64Array", UINT64_ARRAY_IDENTIFIER);
+ sBuilder.addChangeOfStateField("UIntegerArray", UINTEGER_ARRAY_IDENTIFIER);
+
+ sBuilder.addChangeOfStateField("DoesNotExists", DOES_NOT_EXIST_IDENTIFIER);
+ PlcSubscriptionRequest subscriptionRequest = sBuilder.build();
+
+ // Get result of creating subscription
+ PlcSubscriptionResponse sResponse = subscriptionRequest.execute().get();
+ final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) sResponse.getSubscriptionHandle("Bool");
+
+ // Create handler for returned value
+ subscriptionHandle.register(plcSubscriptionEvent -> {
+ assert plcSubscriptionEvent.getResponseCode("Bool").equals(PlcResponseCode.OK);
+ });
+
+ //Wait for value to be returned from server
+ Thread.sleep(1200);
+
+ subscriptionHandle.stopSubscriber();
+
Thread.sleep(20000);
opcuaConnection.close();
@@ -187,49 +227,4 @@
}
}
-
- private static long GetConnectionTime(String ConnectionString) throws Exception {
-
- OpcuaTcpPlcConnection opcuaConnection = null;
- OpcuaPlcFieldHandler fieldH = new OpcuaPlcFieldHandler();
- PlcField field = fieldH.createField("ns=2;i=10855");
-
- long milisStart = System.currentTimeMillis();
- opcuaConnection = (OpcuaTcpPlcConnection)
- new PlcDriverManager().getConnection(ConnectionString);
- long result = System.currentTimeMillis() - milisStart;
- opcuaConnection.close();
- return result;
- }
-
- static class Encapsulater {
- public String connectionString = "";
-
- private long GetConnectionTime() {
- long result = 0;
- for (int counter = 0; counter < 1; counter++) {
- OpcuaTcpPlcConnection opcuaConnection = null;
- OpcuaPlcFieldHandler fieldH = new OpcuaPlcFieldHandler();
- PlcField field = fieldH.createField("ns=2;i=10855");
-
- long milisStart = System.currentTimeMillis();
- try {
- opcuaConnection = (OpcuaTcpPlcConnection)
- new PlcDriverManager().getConnection(connectionString);
- } catch (PlcConnectionException e) {
- throw new PlcRuntimeException(e);
- }
- result += System.currentTimeMillis() - milisStart;
- try {
- assert opcuaConnection != null;
- opcuaConnection.close();
- } catch (Exception e) {
- throw new PlcRuntimeException(e);
- }
-
- }
- return result;
-
- }
- }
}
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
index 9148a20..e817ee5 100644
--- 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
@@ -27,30 +27,24 @@
import org.apache.plc4x.java.api.messages.PlcWriteRequest;
import org.apache.plc4x.java.api.messages.PlcWriteResponse;
import org.apache.plc4x.java.api.types.PlcResponseCode;
-import org.apache.plc4x.java.opcua.connection.OpcuaTcpPlcConnection;
import org.eclipse.milo.examples.server.ExampleServer;
import org.junit.jupiter.api.*;
-import java.math.BigInteger;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-
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.OpcuaPlcDriver.URI_PATTERN;
import static org.apache.plc4x.java.opcua.UtilsTest.assertMatching;
import static org.assertj.core.api.Assertions.fail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.math.BigInteger;
+
/**
*/
public class OpcuaPlcDriverTest {
- private static final Logger logger = LoggerFactory.getLogger(OpcuaPlcDriverTest.class);
-
+ private static final Logger LOGGER = LoggerFactory.getLogger(OpcuaPlcDriverTest.class);
// Read only variables of milo example server of version 3.6
private static final String BOOL_IDENTIFIER_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/Boolean";
@@ -74,7 +68,7 @@
private static final String DATE_TIME_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/DateTime";
private static final String DURATION_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/Duration";
private static final String GUID_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/Guid";
- private static final String LOCALISED_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/LocalizedText";
+ private static final String LOCALIZED_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/LocalizedText";
private static final String NODE_ID_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/NodeId";
private static final String QUALIFIED_NAM_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/QualifiedName";
private static final String UTC_TIME_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/UtcTime";
@@ -96,7 +90,7 @@
private static final String UINT16_ARRAY_IDENTIFIER = "ns=2;s=HelloWorld/ArrayTypes/UInt16Array";
private static final String UINT32_ARRAY_IDENTIFIER = "ns=2;s=HelloWorld/ArrayTypes/UInt32Array";
private static final String UINT64_ARRAY_IDENTIFIER = "ns=2;s=HelloWorld/ArrayTypes/UInt64Array";
-
+ private static final String DATE_TIME_ARRAY_IDENTIFIER = "ns=2;s=HelloWorld/ArrayTypes/DateTimeArray";
// Address of local milo server
private String miloLocalAddress = "127.0.0.1:12686/milo";
@@ -121,8 +115,6 @@
private static ExampleServer exampleServer;
-
-
@BeforeAll
public static void setup() {
try {
@@ -157,9 +149,7 @@
} catch (Exception e) {
fail("Exception during connectionNoParams while closing Test EXCEPTION: " + e.getMessage());
}
-
});
-
}
@Test
@@ -179,8 +169,6 @@
}
});
});
-
-
}
@Test
@@ -270,7 +258,7 @@
builder.addItem("Byte", BYTE_IDENTIFIER_READ_WRITE + ":BYTE", (short) 3);
builder.addItem("Double", DOUBLE_IDENTIFIER_READ_WRITE, 0.5d);
builder.addItem("Float", FLOAT_IDENTIFIER_READ_WRITE, 0.5f);
- builder.addItem("Int16", INT16_IDENTIFIER_READ_WRITE + ":INT", 1);
+ //builder.addItem("Int16", INT16_IDENTIFIER_READ_WRITE + "", (short) 1);
builder.addItem("Int32", INT32_IDENTIFIER_READ_WRITE, 42);
builder.addItem("Int64", INT64_IDENTIFIER_READ_WRITE, 42L);
builder.addItem("Integer", INTEGER_IDENTIFIER_READ_WRITE, 42);
@@ -305,7 +293,7 @@
assert response.getResponseCode("Byte").equals(PlcResponseCode.OK);
assert response.getResponseCode("Double").equals(PlcResponseCode.OK);
assert response.getResponseCode("Float").equals(PlcResponseCode.OK);
- assert response.getResponseCode("Int16").equals(PlcResponseCode.OK);
+ //assert response.getResponseCode("Int16").equals(PlcResponseCode.OK);
assert response.getResponseCode("Int32").equals(PlcResponseCode.OK);
assert response.getResponseCode("Int64").equals(PlcResponseCode.OK);
assert response.getResponseCode("Integer").equals(PlcResponseCode.OK);
@@ -349,17 +337,17 @@
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");
+ assertMatching(URI_PATTERN, "opcua:tcp://localhost");
+ assertMatching(URI_PATTERN, "opcua:tcp://localhost:3131");
+ assertMatching(URI_PATTERN, "opcua:tcp://www.google.de");
+ assertMatching(URI_PATTERN, "opcua:tcp://www.google.de:443");
+ assertMatching(URI_PATTERN, "opcua:tcp://127.0.0.1");
+ assertMatching(URI_PATTERN, "opcua:tcp://127.0.0.1:251");
+ assertMatching(URI_PATTERN, "opcua:tcp://254.254.254.254:1337");
+ assertMatching(URI_PATTERN, "opcua:tcp://254.254.254.254");
- assertMatching(OPCUA_URI_PATTERN, "opcua:tcp://127.0.0.1&discovery=false");
- assertMatching(OPCUA_URI_PATTERN, "opcua:tcp://opcua.demo-this.com:51210/UA/SampleServer?discovery=false");
+ assertMatching(URI_PATTERN, "opcua:tcp://127.0.0.1?discovery=false");
+ assertMatching(URI_PATTERN, "opcua:tcp://opcua.demo-this.com:51210/UA/SampleServer?discovery=false");
}
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
index 9985c9b..5edfd98 100644
--- 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
@@ -18,7 +18,7 @@
*/
package org.apache.plc4x.java.opcua.connection;
-import static org.apache.plc4x.java.opcua.OpcuaPlcDriver.OPCUA_URI_PATTERN;
+import static org.apache.plc4x.java.opcua.OpcuaPlcDriver.URI_PATTERN;
import static org.apache.plc4x.java.opcua.UtilsTest.assertMatching;
import static org.apache.plc4x.java.opcua.UtilsTest.assertNoMatching;
@@ -61,43 +61,21 @@
}
@Test
- public void discoveryParamTest() {
- for (String address : validTCPOPC) {
- for (int port : validPorts) {
- for (String discoveryParam : nDiscoveryParams) {
- String param = "";
- param += discoveryParam;
-
- OpcuaConnectionFactory opcuaConnectionFactory = new OpcuaConnectionFactory();
-
- try {
- OpcuaTcpPlcConnection tcpPlcConnection = opcuaConnectionFactory.opcuaTcpPlcConnectionOf(InetAddress.getByName(address), port, param, 5000);
- assertTrue(tcpPlcConnection.isSkipDiscovery());
- } catch (UnknownHostException e) {
- fail("Testadress is no valid InetAddress: " + e);
- }
- }
- }
- }
-
- }
-
- @Test
public void testConectionStringPattern() {
for (String address : validTCPOPC) {
- assertMatching(OPCUA_URI_PATTERN, "opcua:tcp://127.0.0.1:555?discovery=true");
- assertMatching(OPCUA_URI_PATTERN, "opcua:tcp://127.0.0.1:555?discovery=True");
- assertMatching(OPCUA_URI_PATTERN, "opcua:tcp://127.0.0.1:555?discovery=TRUE");
- assertMatching(OPCUA_URI_PATTERN, "opcua:tcp://127.0.0.1:555?Discovery=True");
+ assertMatching(URI_PATTERN, "opcua:tcp://127.0.0.1:555?discovery=true");
+ assertMatching(URI_PATTERN, "opcua:tcp://127.0.0.1:555?discovery=True");
+ assertMatching(URI_PATTERN, "opcua:tcp://127.0.0.1:555?discovery=TRUE");
+ assertMatching(URI_PATTERN, "opcua:tcp://127.0.0.1:555?Discovery=True");
//No Port Specified
- assertMatching(OPCUA_URI_PATTERN, "opcua:tcp://127.0.0.1?discovery=True");
+ assertMatching(URI_PATTERN, "opcua:tcp://127.0.0.1?discovery=True");
//No Transport Specified
- assertMatching(OPCUA_URI_PATTERN, "opcua://127.0.0.1:647?discovery=True");
+ assertMatching(URI_PATTERN, "opcua://127.0.0.1:647?discovery=True");
//No Params Specified
- assertMatching(OPCUA_URI_PATTERN, "opcua:tcp://127.0.0.1:111");
+ assertMatching(URI_PATTERN, "opcua:tcp://127.0.0.1:111");
//No Transport and Params Specified
- assertMatching(OPCUA_URI_PATTERN, "opcua://127.0.0.1:754");
+ assertMatching(URI_PATTERN, "opcua://127.0.0.1:754");
}
}
}
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
index 7d2d631..8d15bf6 100644
--- 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
@@ -24,7 +24,7 @@
import static org.apache.plc4x.java.opcua.UtilsTest.assertMatching;
import static org.apache.plc4x.java.opcua.UtilsTest.assertNoMatching;
-import static org.apache.plc4x.java.opcua.protocol.OpcuaField.ADDRESS_PATTERN;
+import static org.apache.plc4x.java.opcua.field.OpcuaField.ADDRESS_PATTERN;
/**
*/
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
index 26e8f3e..ca90e22 100644
--- 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
@@ -18,12 +18,56 @@
*/
package org.apache.plc4x.java.opcua.protocol;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
+import org.apache.plc4x.java.PlcDriverManager;
+import org.apache.plc4x.java.api.PlcConnection;
+import org.apache.plc4x.java.api.messages.PlcSubscriptionRequest;
+import org.apache.plc4x.java.api.messages.PlcSubscriptionResponse;
+import org.apache.plc4x.java.api.model.PlcSubscriptionHandle;
+import org.apache.plc4x.java.api.types.PlcResponseCode;
+import org.apache.plc4x.java.opcua.OpcuaPlcDriverTest;
+import org.assertj.core.api.Assertions;
+import org.eclipse.milo.examples.server.ExampleServer;
+import org.junit.jupiter.api.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
*/
public class OpcuaSubscriptionHandleTest {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(OpcuaPlcDriverTest.class);
+
+ private static ExampleServer exampleServer;
+
+ // Address of local milo server
+ private static String miloLocalAddress = "127.0.0.1:12686/milo";
+ //Tcp pattern of OPC UA
+ private static String opcPattern = "opcua:tcp://";
+
+ private String paramSectionDivider = "?";
+ private String paramDivider = "&";
+
+ private static String tcpConnectionAddress = opcPattern + miloLocalAddress;
+
+ // Read only variables of milo example server of version 3.6
+ private static final String BOOL_IDENTIFIER_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/Boolean";
+ private static final String BYTE_IDENTIFIER_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/Byte";
+ private static final String DOUBLE_IDENTIFIER_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/Double";
+ private static final String FLOAT_IDENTIFIER_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/Float";
+ private static final String INT16_IDENTIFIER_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/Int16";
+ private static final String INT32_IDENTIFIER_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/Int32";
+ private static final String INT64_IDENTIFIER_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/Int64";
+ private static final String INTEGER_IDENTIFIER_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/Integer";
+ private static final String SBYTE_IDENTIFIER_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/SByte";
+ private static final String STRING_IDENTIFIER_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/String";
+ private static final String UINT16_IDENTIFIER_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/UInt16";
+ private static final String UINT32_IDENTIFIER_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/UInt32";
+ private static final String UINT64_IDENTIFIER_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/UInt64";
+ private static final String UINTEGER_IDENTIFIER_READ_WRITE = "ns=2;s=HelloWorld/ScalarTypes/UInteger";
+ private static final String DOES_NOT_EXIST_IDENTIFIER_READ_WRITE = "ns=2;i=12512623";
+
+ private static PlcConnection opcuaConnection;
+
@BeforeEach
public void before() {
}
@@ -32,4 +76,467 @@
public void after() {
}
+
+ @BeforeAll
+ public static void setup() {
+ try {
+ exampleServer = new ExampleServer();
+ exampleServer.startup().get();
+ //Connect
+ opcuaConnection = new PlcDriverManager().getConnection(tcpConnectionAddress);
+ assert opcuaConnection.isConnected();
+ } catch (Exception e) {
+
+ }
+ }
+
+ @AfterAll
+ public static void tearDown() {
+ try {
+ // Close Connection
+ opcuaConnection.close();
+ assert !opcuaConnection.isConnected();
+
+ exampleServer.shutdown().get();
+ } catch (Exception e) {
+
+ }
+ }
+
+ @Test
+ public void subscribeBool() throws Exception {
+ String field = "Bool";
+ String identifier = BOOL_IDENTIFIER_READ_WRITE;
+ LOGGER.info("Starting subscription {} test", field);
+
+ // Create Subscription
+ PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder();
+ builder.addChangeOfStateField(field, identifier);
+ PlcSubscriptionRequest request = builder.build();
+
+ // Get result of creating subscription
+ PlcSubscriptionResponse response = request.execute().get();
+ final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(field);
+
+ // Create handler for returned value
+ subscriptionHandle.register(plcSubscriptionEvent -> {
+ assert plcSubscriptionEvent.getResponseCode(field).equals(PlcResponseCode.OK);
+ LOGGER.info("Received a response from {} test {}", field, plcSubscriptionEvent.getPlcValue(field).toString());
+ });
+
+ //Wait for value to be returned from server
+ Thread.sleep(1200);
+
+ subscriptionHandle.stopSubscriber();
+ }
+
+ @Test
+ public void subscribeByte() throws Exception {
+ String field = "Byte";
+ String identifier = BYTE_IDENTIFIER_READ_WRITE;
+ LOGGER.info("Starting subscription {} test", field);
+
+ // Create Subscription
+ PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder();
+ builder.addChangeOfStateField(field, identifier);
+ PlcSubscriptionRequest request = builder.build();
+
+ // Get result of creating subscription
+ PlcSubscriptionResponse response = request.execute().get();
+ final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(field);
+
+ // Create handler for returned value
+ subscriptionHandle.register(plcSubscriptionEvent -> {
+ assert plcSubscriptionEvent.getResponseCode(field).equals(PlcResponseCode.OK);
+ LOGGER.info("Received a response from {} test {}", field, plcSubscriptionEvent.getPlcValue(field).toString());
+ });
+
+ //Wait for value to be returned from server
+ Thread.sleep(1200);
+
+ subscriptionHandle.stopSubscriber();
+ }
+
+ @Test
+ public void subscribeDouble() throws Exception {
+ String field = "Double";
+ String identifier = DOUBLE_IDENTIFIER_READ_WRITE;
+ LOGGER.info("Starting subscription {} test", field);
+
+ // Create Subscription
+ PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder();
+ builder.addChangeOfStateField(field, identifier);
+ PlcSubscriptionRequest request = builder.build();
+
+ // Get result of creating subscription
+ PlcSubscriptionResponse response = request.execute().get();
+ final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(field);
+
+ // Create handler for returned value
+ subscriptionHandle.register(plcSubscriptionEvent -> {
+ assert plcSubscriptionEvent.getResponseCode(field).equals(PlcResponseCode.OK);
+ LOGGER.info("Received a response from {} test {}", field, plcSubscriptionEvent.getPlcValue(field).toString());
+ });
+
+ //Wait for value to be returned from server
+ Thread.sleep(1200);
+
+ subscriptionHandle.stopSubscriber();
+ }
+
+ @Test
+ public void subscribeFloat() throws Exception {
+ String field = "Float";
+ String identifier = FLOAT_IDENTIFIER_READ_WRITE;
+ LOGGER.info("Starting subscription {} test", field);
+
+ // Create Subscription
+ PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder();
+ builder.addChangeOfStateField(field, identifier);
+ PlcSubscriptionRequest request = builder.build();
+
+ // Get result of creating subscription
+ PlcSubscriptionResponse response = request.execute().get();
+ final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(field);
+
+ // Create handler for returned value
+ subscriptionHandle.register(plcSubscriptionEvent -> {
+ assert plcSubscriptionEvent.getResponseCode(field).equals(PlcResponseCode.OK);
+ LOGGER.info("Received a response from {} test {}", field, plcSubscriptionEvent.getPlcValue(field).toString());
+ });
+
+ //Wait for value to be returned from server
+ Thread.sleep(1200);
+
+ subscriptionHandle.stopSubscriber();
+ }
+
+ @Test
+ public void subscribeInt16() throws Exception {
+ String field = "Int16";
+ String identifier = INT16_IDENTIFIER_READ_WRITE;
+ LOGGER.info("Starting subscription {} test", field);
+
+ // Create Subscription
+ PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder();
+ builder.addChangeOfStateField(field, identifier);
+ PlcSubscriptionRequest request = builder.build();
+
+ // Get result of creating subscription
+ PlcSubscriptionResponse response = request.execute().get();
+ final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(field);
+
+ // Create handler for returned value
+ subscriptionHandle.register(plcSubscriptionEvent -> {
+ assert plcSubscriptionEvent.getResponseCode(field).equals(PlcResponseCode.OK);
+ LOGGER.info("Received a response from {} test {}", field, plcSubscriptionEvent.getPlcValue(field).toString());
+ });
+
+ //Wait for value to be returned from server
+ Thread.sleep(1200);
+
+ subscriptionHandle.stopSubscriber();
+ }
+
+ @Test
+ public void subscribeInt32() throws Exception {
+ String field = "Int32";
+ String identifier = INT32_IDENTIFIER_READ_WRITE;
+ LOGGER.info("Starting subscription {} test", field);
+
+ // Create Subscription
+ PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder();
+ builder.addChangeOfStateField(field, identifier);
+ PlcSubscriptionRequest request = builder.build();
+
+ // Get result of creating subscription
+ PlcSubscriptionResponse response = request.execute().get();
+ final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(field);
+
+ // Create handler for returned value
+ subscriptionHandle.register(plcSubscriptionEvent -> {
+ assert plcSubscriptionEvent.getResponseCode(field).equals(PlcResponseCode.OK);
+ LOGGER.info("Received a response from {} test {}", field, plcSubscriptionEvent.getPlcValue(field).toString());
+ });
+
+ //Wait for value to be returned from server
+ Thread.sleep(1200);
+
+ subscriptionHandle.stopSubscriber();
+ }
+
+ @Test
+ public void subscribeInt64() throws Exception {
+ String field = "Int64";
+ String identifier = INT64_IDENTIFIER_READ_WRITE;
+ LOGGER.info("Starting subscription {} test", field);
+
+ // Create Subscription
+ PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder();
+ builder.addChangeOfStateField(field, identifier);
+ PlcSubscriptionRequest request = builder.build();
+
+ // Get result of creating subscription
+ PlcSubscriptionResponse response = request.execute().get();
+ final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(field);
+
+ // Create handler for returned value
+ subscriptionHandle.register(plcSubscriptionEvent -> {
+ assert plcSubscriptionEvent.getResponseCode(field).equals(PlcResponseCode.OK);
+ LOGGER.info("Received a response from {} test {}", field, plcSubscriptionEvent.getPlcValue(field).toString());
+ });
+
+ //Wait for value to be returned from server
+ Thread.sleep(1200);
+
+ subscriptionHandle.stopSubscriber();
+ }
+
+ @Test
+ public void subscribeInteger() throws Exception {
+ String field = "Integer";
+ String identifier = INTEGER_IDENTIFIER_READ_WRITE;
+ LOGGER.info("Starting subscription {} test", field);
+
+ // Create Subscription
+ PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder();
+ builder.addChangeOfStateField(field, identifier);
+ PlcSubscriptionRequest request = builder.build();
+
+ // Get result of creating subscription
+ PlcSubscriptionResponse response = request.execute().get();
+ final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(field);
+
+ // Create handler for returned value
+ subscriptionHandle.register(plcSubscriptionEvent -> {
+ assert plcSubscriptionEvent.getResponseCode(field).equals(PlcResponseCode.OK);
+ LOGGER.info("Received a response from {} test {}", field, plcSubscriptionEvent.getPlcValue(field).toString());
+ });
+
+ //Wait for value to be returned from server
+ Thread.sleep(1200);
+
+ subscriptionHandle.stopSubscriber();
+ }
+
+ @Test
+ public void subscribeSByte() throws Exception {
+ String field = "SByte";
+ String identifier = SBYTE_IDENTIFIER_READ_WRITE;
+ LOGGER.info("Starting subscription {} test", field);
+
+ // Create Subscription
+ PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder();
+ builder.addChangeOfStateField(field, identifier);
+ PlcSubscriptionRequest request = builder.build();
+
+ // Get result of creating subscription
+ PlcSubscriptionResponse response = request.execute().get();
+ final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(field);
+
+ // Create handler for returned value
+ subscriptionHandle.register(plcSubscriptionEvent -> {
+ assert plcSubscriptionEvent.getResponseCode(field).equals(PlcResponseCode.OK);
+ LOGGER.info("Received a response from {} test {}", field, plcSubscriptionEvent.getPlcValue(field).toString());
+ });
+
+ //Wait for value to be returned from server
+ Thread.sleep(1200);
+
+ subscriptionHandle.stopSubscriber();
+ }
+
+ @Test
+ public void subscribeString() throws Exception {
+ String field = "String";
+ String identifier = STRING_IDENTIFIER_READ_WRITE;
+ LOGGER.info("Starting subscription {} test", field);
+
+ // Create Subscription
+ PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder();
+ builder.addChangeOfStateField(field, identifier);
+ PlcSubscriptionRequest request = builder.build();
+
+ // Get result of creating subscription
+ PlcSubscriptionResponse response = request.execute().get();
+ final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(field);
+
+ // Create handler for returned value
+ subscriptionHandle.register(plcSubscriptionEvent -> {
+ assert plcSubscriptionEvent.getResponseCode(field).equals(PlcResponseCode.OK);
+ LOGGER.info("Received a response from {} test {}", field, plcSubscriptionEvent.getPlcValue(field).toString());
+ });
+
+ //Wait for value to be returned from server
+ Thread.sleep(1200);
+
+ subscriptionHandle.stopSubscriber();
+ }
+
+ @Test
+ public void subscribeUInt16() throws Exception {
+ String field = "Uint16";
+ String identifier = UINT16_IDENTIFIER_READ_WRITE;
+ LOGGER.info("Starting subscription {} test", field);
+
+ // Create Subscription
+ PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder();
+ builder.addChangeOfStateField(field, identifier);
+ PlcSubscriptionRequest request = builder.build();
+
+ // Get result of creating subscription
+ PlcSubscriptionResponse response = request.execute().get();
+ final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(field);
+
+ // Create handler for returned value
+ subscriptionHandle.register(plcSubscriptionEvent -> {
+ assert plcSubscriptionEvent.getResponseCode(field).equals(PlcResponseCode.OK);
+ LOGGER.info("Received a response from {} test {}", field, plcSubscriptionEvent.getPlcValue(field).toString());
+ });
+
+ //Wait for value to be returned from server
+ Thread.sleep(1200);
+
+ subscriptionHandle.stopSubscriber();
+ }
+
+ @Test
+ public void subscribeUInt32() throws Exception {
+ String field = "UInt32";
+ String identifier = UINT32_IDENTIFIER_READ_WRITE;
+ LOGGER.info("Starting subscription {} test", field);
+
+ // Create Subscription
+ PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder();
+ builder.addChangeOfStateField(field, identifier);
+ PlcSubscriptionRequest request = builder.build();
+
+ // Get result of creating subscription
+ PlcSubscriptionResponse response = request.execute().get();
+ final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(field);
+
+ // Create handler for returned value
+ subscriptionHandle.register(plcSubscriptionEvent -> {
+ assert plcSubscriptionEvent.getResponseCode(field).equals(PlcResponseCode.OK);
+ LOGGER.info("Received a response from {} test {}", field, plcSubscriptionEvent.getPlcValue(field).toString());
+ });
+
+ //Wait for value to be returned from server
+ Thread.sleep(1200);
+
+ subscriptionHandle.stopSubscriber();
+ }
+
+ @Test
+ public void subscribeUInt64() throws Exception {
+ String field = "UInt64";
+ String identifier = UINT64_IDENTIFIER_READ_WRITE;
+ LOGGER.info("Starting subscription {} test", field);
+
+ // Create Subscription
+ PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder();
+ builder.addChangeOfStateField(field, identifier);
+ PlcSubscriptionRequest request = builder.build();
+
+ // Get result of creating subscription
+ PlcSubscriptionResponse response = request.execute().get();
+ final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(field);
+
+ // Create handler for returned value
+ subscriptionHandle.register(plcSubscriptionEvent -> {
+ assert plcSubscriptionEvent.getResponseCode(field).equals(PlcResponseCode.OK);
+ LOGGER.info("Received a response from {} test {}", field, plcSubscriptionEvent.getPlcValue(field).toString());
+ });
+
+ //Wait for value to be returned from server
+ Thread.sleep(1200);
+
+ subscriptionHandle.stopSubscriber();
+ }
+
+ @Test
+ public void subscribeUInteger() throws Exception {
+ String field = "UInteger";
+ String identifier = UINTEGER_IDENTIFIER_READ_WRITE;
+ LOGGER.info("Starting subscription {} test", field);
+
+ // Create Subscription
+ PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder();
+ builder.addChangeOfStateField(field, identifier);
+ PlcSubscriptionRequest request = builder.build();
+
+ // Get result of creating subscription
+ PlcSubscriptionResponse response = request.execute().get();
+ final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(field);
+
+ // Create handler for returned value
+ subscriptionHandle.register(plcSubscriptionEvent -> {
+ assert plcSubscriptionEvent.getResponseCode(field).equals(PlcResponseCode.OK);
+ LOGGER.info("Received a response from {} test {}", field, plcSubscriptionEvent.getPlcValue(field).toString());
+ });
+
+ //Wait for value to be returned from server
+ Thread.sleep(1200);
+
+ subscriptionHandle.stopSubscriber();
+ }
+
+ @Test
+ public void subscribeDoesNotExists() throws Exception {
+ String field = "DoesNotExists";
+ String identifier = DOES_NOT_EXIST_IDENTIFIER_READ_WRITE;
+ LOGGER.info("Starting subscription {} test", field);
+
+ // Create Subscription
+ PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder();
+ builder.addChangeOfStateField(field, identifier);
+ PlcSubscriptionRequest request = builder.build();
+
+ // Get result of creating subscription
+ PlcSubscriptionResponse response = request.execute().get();
+ final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(field);
+
+ // Create handler for returned value
+ subscriptionHandle.register(plcSubscriptionEvent -> {
+ //This should never be called,
+ assert false;
+ LOGGER.info("Received a response from {} test {}", field, plcSubscriptionEvent.getPlcValue(field).toString());
+ });
+
+ //Wait for value to be returned from server
+ Thread.sleep(1200);
+
+ subscriptionHandle.stopSubscriber();
+ }
+
+ @Test
+ public void subscribeMultiple() throws Exception {
+ String field1 = "UInteger";
+ String identifier1 = UINTEGER_IDENTIFIER_READ_WRITE;
+ String field2 = "Integer";
+ String identifier2 = INTEGER_IDENTIFIER_READ_WRITE;
+ LOGGER.info("Starting subscription {} and {} test", field1, field2);
+
+ // Create Subscription
+ PlcSubscriptionRequest.Builder builder = opcuaConnection.subscriptionRequestBuilder();
+ builder.addChangeOfStateField(field1, identifier1);
+ builder.addChangeOfStateField(field2, identifier2);
+ PlcSubscriptionRequest request = builder.build();
+
+ // Get result of creating subscription
+ PlcSubscriptionResponse response = request.execute().get();
+ final OpcuaSubscriptionHandle subscriptionHandle = (OpcuaSubscriptionHandle) response.getSubscriptionHandle(field1);
+
+ // Create handler for returned value
+ subscriptionHandle.register(plcSubscriptionEvent -> {
+ assert plcSubscriptionEvent.getResponseCode(field1).equals(PlcResponseCode.OK);
+ assert plcSubscriptionEvent.getResponseCode(field2).equals(PlcResponseCode.OK);
+ });
+
+ //Wait for value to be returned from server
+ Thread.sleep(1200);
+
+ subscriptionHandle.stopSubscriber();
+ }
+
}
diff --git a/plc4j/drivers/opcua/src/test/resources/log4j.properties b/plc4j/drivers/opcua/src/test/resources/log4j.properties
new file mode 100644
index 0000000..593679f
--- /dev/null
+++ b/plc4j/drivers/opcua/src/test/resources/log4j.properties
@@ -0,0 +1,24 @@
+#
+# 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.
+#
+appender.out.type=Console
+appender.out.name=out
+appender.out.layout.type=PatternLayout
+appender.out.layout.pattern=[%30.30t] %-30.30c{1} %-5p %m%n
+rootLogger.level=TRACE
+rootLogger.appenderRef.out.ref=out
diff --git a/plc4j/drivers/opcua/src/test/resources/logback.xml b/plc4j/drivers/opcua/src/test/resources/logback.xml
new file mode 100644
index 0000000..99d73cb
--- /dev/null
+++ b/plc4j/drivers/opcua/src/test/resources/logback.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+
+<configuration>
+ <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%d %5p | %t | %-55logger{55} | %m %n</pattern>
+ </encoder>
+ </appender>
+ <root>
+ <level value="TRACE"/>
+ <appender-ref ref="CONSOLE"/>
+ </root>
+</configuration>
\ No newline at end of file
diff --git a/plc4j/examples/hello-world-plc4x/src/main/java/org/apache/plc4x/java/examples/helloplc4x/HelloPlc4x.java b/plc4j/examples/hello-world-plc4x/src/main/java/org/apache/plc4x/java/examples/helloplc4x/HelloPlc4x.java
index c0c7817..3a2f9eb 100644
--- a/plc4j/examples/hello-world-plc4x/src/main/java/org/apache/plc4x/java/examples/helloplc4x/HelloPlc4x.java
+++ b/plc4j/examples/hello-world-plc4x/src/main/java/org/apache/plc4x/java/examples/helloplc4x/HelloPlc4x.java
@@ -92,6 +92,7 @@
// Give the async request a little time...
TimeUnit.MILLISECONDS.sleep(1000);
+ plcConnection.close();
System.exit(0);
}
}
diff --git a/plc4j/integrations/opcua-server/src/test/java/org/apache/plc4x/java/opcuaserver/UtilsTest.java b/plc4j/integrations/opcua-server/src/test/java/org/apache/plc4x/java/opcuaserver/UtilsTest.java
deleted file mode 100644
index 1cc3106..0000000
--- a/plc4j/integrations/opcua-server/src/test/java/org/apache/plc4x/java/opcuaserver/UtilsTest.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- 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.opcuaserver;
-
-
-import java.util.regex.Pattern;
-
-import static org.junit.jupiter.api.Assertions.fail;
-
-/**
- */
-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/spi/src/main/java/org/apache/plc4x/java/spi/ConversationContext.java b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/ConversationContext.java
index f727382..9740200 100644
--- a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/ConversationContext.java
+++ b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/ConversationContext.java
@@ -21,6 +21,7 @@
import io.netty.channel.Channel;
import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
+import org.apache.plc4x.java.spi.configuration.Configuration;
import java.time.Duration;
import java.util.concurrent.TimeoutException;
@@ -41,6 +42,8 @@
void fireDisconnected();
+ void fireDiscovered(Configuration c);
+
SendRequestContext<T> sendRequest(T packet);
interface SendRequestContext<T> {
diff --git a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/Plc4xNettyWrapper.java b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/Plc4xNettyWrapper.java
index 9077017..90a11a1 100644
--- a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/Plc4xNettyWrapper.java
+++ b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/Plc4xNettyWrapper.java
@@ -24,11 +24,8 @@
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.MessageToMessageCodec;
import io.vavr.control.Either;
-import org.apache.plc4x.java.spi.events.CloseConnectionEvent;
-import org.apache.plc4x.java.spi.events.ConnectEvent;
-import org.apache.plc4x.java.spi.events.ConnectedEvent;
-import org.apache.plc4x.java.spi.events.DisconnectEvent;
-import org.apache.plc4x.java.spi.events.DisconnectedEvent;
+import org.apache.plc4x.java.spi.configuration.Configuration;
+import org.apache.plc4x.java.spi.events.*;
import org.apache.plc4x.java.spi.internal.DefaultExpectRequestContext;
import org.apache.plc4x.java.spi.internal.DefaultSendRequestContext;
import org.apache.plc4x.java.spi.internal.HandlerRegistration;
@@ -89,6 +86,11 @@
}
@Override
+ public void fireDiscovered(Configuration c) {
+ pipeline.fireUserEventTriggered(DiscoveredEvent.class);
+ }
+
+ @Override
public SendRequestContext<T> sendRequest(T packet) {
return new DefaultSendRequestContext<>(handler -> {
logger.trace("Adding Response Handler ...");
@@ -190,6 +192,8 @@
this.protocolBase.onConnect(new DefaultConversationContext<>(ctx, passive));
} else if (evt instanceof DisconnectEvent) {
this.protocolBase.onDisconnect(new DefaultConversationContext<>(ctx, passive));
+ } else if (evt instanceof DiscoverEvent) {
+ this.protocolBase.onDiscover(new DefaultConversationContext<>(ctx, passive));
} else if (evt instanceof CloseConnectionEvent) {
this.protocolBase.close(new DefaultConversationContext<>(ctx, passive));
} else {
@@ -235,6 +239,12 @@
}
@Override
+ public void fireDiscovered(Configuration c) {
+ logger.trace("Firing Discovered!");
+ channelHandlerContext.pipeline().fireUserEventTriggered(new DiscoveredEvent(c));
+ }
+
+ @Override
public SendRequestContext<T1> sendRequest(T1 packet) {
return new DefaultSendRequestContext<>(handler -> {
logger.trace("Adding Response Handler ...");
diff --git a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/Plc4xProtocolBase.java b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/Plc4xProtocolBase.java
index d6bca70..1c31908 100644
--- a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/Plc4xProtocolBase.java
+++ b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/Plc4xProtocolBase.java
@@ -58,6 +58,10 @@
// Intentionally do nothing here
}
+ public void onDiscover(ConversationContext<T> context) {
+ // Intentionally do nothing here
+ }
+
/**
* TODO document me
* <p>
diff --git a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/connection/DefaultNettyPlcConnection.java b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/connection/DefaultNettyPlcConnection.java
index 177ef71..fdedf0c 100644
--- a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/connection/DefaultNettyPlcConnection.java
+++ b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/connection/DefaultNettyPlcConnection.java
@@ -45,10 +45,11 @@
protected final static long DEFAULT_DISCONNECT_WAIT_TIME = 10000L;
private static final Logger logger = LoggerFactory.getLogger(DefaultNettyPlcConnection.class);
- protected final Configuration configuration;
+ protected Configuration configuration;
protected final ChannelFactory channelFactory;
protected final boolean awaitSessionSetupComplete;
protected final boolean awaitSessionDisconnectComplete;
+ protected final boolean awaitSessionDiscoverComplete;
protected final ProtocolStackConfigurer stackConfigurer;
protected final CompletableFuture<Void> sessionDisconnectCompleteFuture = new CompletableFuture<>();
@@ -58,13 +59,15 @@
public DefaultNettyPlcConnection(boolean canRead, boolean canWrite, boolean canSubscribe,
PlcFieldHandler fieldHandler, PlcValueHandler valueHandler, Configuration configuration,
ChannelFactory channelFactory, boolean awaitSessionSetupComplete,
- boolean awaitSessionDisconnectComplete, ProtocolStackConfigurer stackConfigurer, BaseOptimizer optimizer) {
+ boolean awaitSessionDisconnectComplete, boolean awaitSessionDiscoverComplete, ProtocolStackConfigurer stackConfigurer, BaseOptimizer optimizer) {
super(canRead, canWrite, canSubscribe, fieldHandler, valueHandler, optimizer);
this.configuration = configuration;
this.channelFactory = channelFactory;
this.awaitSessionSetupComplete = awaitSessionSetupComplete;
//Used to signal that a disconnect has completed while closing a connection.
this.awaitSessionDisconnectComplete = awaitSessionDisconnectComplete;
+ //Used to signal that discovery has been completed
+ this.awaitSessionDiscoverComplete = awaitSessionDiscoverComplete;
this.stackConfigurer = stackConfigurer;
this.connected = false;
@@ -77,6 +80,7 @@
// define a future we can use to signal back that the s7 session is
// finished initializing.
CompletableFuture<Void> sessionSetupCompleteFuture = new CompletableFuture<>();
+ CompletableFuture<Configuration> sessionDiscoveredCompleteFuture = new CompletableFuture<>();
if(channelFactory == null) {
throw new PlcConnectionException("No channel factory provided");
@@ -86,7 +90,26 @@
ConfigurationFactory.configure(configuration, channelFactory);
// Have the channel factory create a new channel instance.
- channel = channelFactory.createChannel(getChannelHandler(sessionSetupCompleteFuture, sessionDisconnectCompleteFuture));
+ if (awaitSessionDiscoverComplete) {
+ channel = channelFactory.createChannel(getChannelHandler(sessionSetupCompleteFuture, sessionDisconnectCompleteFuture, sessionDiscoveredCompleteFuture));
+ channel.closeFuture().addListener(future -> {
+ if (!sessionDiscoveredCompleteFuture.isDone()) {
+ //Do Nothing
+ try {
+ sessionDiscoveredCompleteFuture.complete(null);
+ } catch (Exception e) {
+ //Do Nothing
+ }
+
+ }
+ });
+ channel.pipeline().fireUserEventTriggered(new DiscoverEvent());
+
+ // Wait till the connection is established.
+ sessionDiscoveredCompleteFuture.get();
+ }
+
+ channel = channelFactory.createChannel(getChannelHandler(sessionSetupCompleteFuture, sessionDisconnectCompleteFuture, sessionDiscoveredCompleteFuture));
channel.closeFuture().addListener(future -> {
if (!sessionSetupCompleteFuture.isDone()) {
sessionSetupCompleteFuture.completeExceptionally(
@@ -118,9 +141,7 @@
*/
@Override
public void close() throws PlcConnectionException {
-
logger.debug("Closing connection to PLC, await for disconnect = {}", awaitSessionDisconnectComplete);
-
channel.pipeline().fireUserEventTriggered(new DisconnectEvent());
try {
if (awaitSessionDisconnectComplete) {
@@ -130,11 +151,10 @@
logger.error("Timeout while trying to close connection");
}
channel.pipeline().fireUserEventTriggered(new CloseConnectionEvent());
-
channel.close().awaitUninterruptibly();
if (!sessionDisconnectCompleteFuture.isDone()) {
- sessionDisconnectCompleteFuture.complete(null );
+ sessionDisconnectCompleteFuture.complete(null);
}
channel = null;
@@ -155,7 +175,7 @@
return channel;
}
- public ChannelHandler getChannelHandler(CompletableFuture<Void> sessionSetupCompleteFuture, CompletableFuture<Void> sessionDisconnectCompleteFuture) {
+ public ChannelHandler getChannelHandler(CompletableFuture<Void> sessionSetupCompleteFuture, CompletableFuture<Void> sessionDisconnectCompleteFuture, CompletableFuture<Configuration> sessionDiscoverCompleteFuture) {
if (stackConfigurer == null) {
throw new IllegalStateException("No Protocol Stack Configurer is given!");
}
@@ -174,6 +194,8 @@
sessionSetupCompleteFuture.complete(null);
} else if (evt instanceof DisconnectedEvent) {
sessionDisconnectCompleteFuture.complete(null);
+ } else if (evt instanceof DiscoveredEvent) {
+ sessionDiscoverCompleteFuture.complete(((DiscoveredEvent) evt).getConfiguration());
} else {
super.userEventTriggered(ctx, evt);
}
diff --git a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/connection/GeneratedDriverBase.java b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/connection/GeneratedDriverBase.java
index 6c58d71..ce2ee99 100644
--- a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/connection/GeneratedDriverBase.java
+++ b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/connection/GeneratedDriverBase.java
@@ -39,8 +39,8 @@
public abstract class GeneratedDriverBase<BASE_PACKET extends Message> implements PlcDriver {
public static final String PROPERTY_PLC4X_FORCE_AWAIT_SETUP_COMPLETE = "PLC4X_FORCE_AWAIT_SETUP_COMPLETE";
-
public static final String PROPERTY_PLC4X_FORCE_AWAIT_DISCONNECT_COMPLETE = "PLC4X_FORCE_AWAIT_DISCONNECT_COMPLETE";
+ public static final String PROPERTY_PLC4X_FORCE_AWAIT_DISCOVER_COMPLETE = "PLC4X_FORCE_AWAIT_DISCOVER_COMPLETE";
private static final Pattern URI_PATTERN = Pattern.compile(
"^(?<protocolCode>[a-z0-9\\-]*)(:(?<transportCode>[a-z0-9]*))?://(?<transportConfig>[^?]*)(\\?(?<paramString>.*))?");
@@ -67,6 +67,10 @@
return false;
}
+ protected boolean awaitDiscoverComplete() {
+ return false;
+ }
+
protected BaseOptimizer getOptimizer() {
return null;
}
@@ -150,6 +154,12 @@
awaitDisconnectComplete = Boolean.parseBoolean(System.getProperty(PROPERTY_PLC4X_FORCE_AWAIT_DISCONNECT_COMPLETE));
}
+ // Make the "await disconnect complete" overridable via system property.
+ boolean awaitDiscoverComplete = awaitDiscoverComplete();
+ if(System.getProperty(PROPERTY_PLC4X_FORCE_AWAIT_DISCOVER_COMPLETE) != null) {
+ awaitDiscoverComplete = Boolean.parseBoolean(System.getProperty(PROPERTY_PLC4X_FORCE_AWAIT_DISCOVER_COMPLETE));
+ }
+
return new DefaultNettyPlcConnection(
canRead(), canWrite(), canSubscribe(),
getFieldHandler(),
@@ -158,6 +168,7 @@
channelFactory,
awaitSetupComplete,
awaitDisconnectComplete,
+ awaitDiscoverComplete,
getStackConfigurer(),
getOptimizer());
}
diff --git a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/events/DiscoverEvent.java b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/events/DiscoverEvent.java
new file mode 100644
index 0000000..9351e65
--- /dev/null
+++ b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/events/DiscoverEvent.java
@@ -0,0 +1,22 @@
+/*
+ * 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.spi.events;
+
+public class DiscoverEvent {
+}
diff --git a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/events/DiscoveredEvent.java b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/events/DiscoveredEvent.java
new file mode 100644
index 0000000..524eb29
--- /dev/null
+++ b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/events/DiscoveredEvent.java
@@ -0,0 +1,32 @@
+/*
+ * 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.spi.events;
+
+import org.apache.plc4x.java.spi.configuration.Configuration;
+
+public class DiscoveredEvent {
+
+ private Configuration configuration;
+
+ public DiscoveredEvent(Configuration c) {
+ this.configuration = c;
+ }
+
+ public Configuration getConfiguration() { return configuration; }
+}
diff --git a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/generation/WriteBufferByteBased.java b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/generation/WriteBufferByteBased.java
index 3b41886..dc7bafe 100644
--- a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/generation/WriteBufferByteBased.java
+++ b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/generation/WriteBufferByteBased.java
@@ -45,6 +45,10 @@
this.littleEndian = littleEndian;
}
+ public void setPos(int position) {
+ bb.position(position);
+ }
+
public byte[] getData() {
return bb.array();
}
diff --git a/protocols/opcua/pom.xml b/protocols/opcua/pom.xml
index a247d06..299f628 100644
--- a/protocols/opcua/pom.xml
+++ b/protocols/opcua/pom.xml
@@ -32,12 +32,216 @@
<name>Protocols: OPC UA</name>
<description>Base protocol specifications for the OPC UA protocol</description>
+ <build>
+ <plugins>
+ <!-- Fetch the master-data which will be used to translate manufacturer ids to readable names -->
+ <plugin>
+ <groupId>com.googlecode.maven-download-plugin</groupId>
+ <artifactId>download-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>fetch-opc-datatypes</id>
+ <phase>generate-resources</phase>
+ <goals>
+ <goal>wget</goal>
+ </goals>
+ <configuration>
+ <url>https://raw.githubusercontent.com/OPCFoundation/UA-Nodeset/v1.04/Schema/Opc.Ua.Types.bsd</url>
+ <unpack>false</unpack>
+ <outputDirectory>${project.build.directory}/downloads</outputDirectory>
+ <outputFileName>Opc.Ua.Types.bsd</outputFileName>
+ <skipCache>true</skipCache>
+ <overwrite>true</overwrite>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <!-- Fetch the master-data which will be used to translate manufacturer ids to readable names -->
+ <plugin>
+ <groupId>com.googlecode.maven-download-plugin</groupId>
+ <artifactId>download-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>fetch-opc-statuscodes</id>
+ <phase>generate-resources</phase>
+ <goals>
+ <goal>wget</goal>
+ </goals>
+ <configuration>
+ <url>https://raw.githubusercontent.com/OPCFoundation/UA-Nodeset/v1.04/Schema/StatusCode.csv</url>
+ <unpack>false</unpack>
+ <outputDirectory>${project.build.directory}/downloads</outputDirectory>
+ <outputFileName>StatusCode.csv</outputFileName>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>com.googlecode.maven-download-plugin</groupId>
+ <artifactId>download-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>fetch-opc-discriminators</id>
+ <phase>generate-resources</phase>
+ <goals>
+ <goal>wget</goal>
+ </goals>
+ <configuration>
+ <url>https://raw.githubusercontent.com/OPCFoundation/UA-Nodeset/v1.04/Schema/Opc.Ua.NodeSet2.Services.xml</url>
+ <unpack>false</unpack>
+ <outputDirectory>${project.build.directory}/downloads</outputDirectory>
+ <outputFileName>Opc.Ua.NodeSet2.Services.xml</outputFileName>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>com.googlecode.maven-download-plugin</groupId>
+ <artifactId>download-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>fetch-opc-services-enum</id>
+ <phase>generate-resources</phase>
+ <goals>
+ <goal>wget</goal>
+ </goals>
+ <configuration>
+ <url>https://raw.githubusercontent.com/OPCFoundation/UA-Nodeset/v1.04/Schema/Opc.Ua.NodeIds.Services.csv</url>
+ <unpack>false</unpack>
+ <outputDirectory>${project.build.directory}/downloads</outputDirectory>
+ <outputFileName>Opc.Ua.NodeIds.Services.csv</outputFileName>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>xml-maven-plugin</artifactId>
+ <version>1.0.2</version>
+ <executions>
+ <execution>
+ <id>transform-services</id>
+ <phase>process-resources</phase>
+ <goals>
+ <goal>transform</goal>
+ </goals>
+ </execution>
+ </executions>
+ <configuration>
+ <transformationSets>
+ <transformationSet>
+ <dir>${project.build.directory}/downloads</dir>
+ <includes>Opc.Ua.Types.bsd</includes>
+ <stylesheet>src/main/xslt/opc-services.xsl</stylesheet>
+ <outputDir>${project.build.outputDirectory}/protocols/opcua</outputDir>
+ <fileMappers>
+ <fileMapper implementation="org.codehaus.plexus.components.io.filemappers.MergeFileMapper">
+ <targetName>opc-services.mspec</targetName>
+ </fileMapper>
+ </fileMappers>
+ <parameters>
+ <parameter>
+ <name>servicesEnum</name>
+ <value>${project.build.directory}/downloads/Opc.Ua.NodeIds.Services.csv</value>
+ </parameter>
+ </parameters>
+ <outputProperties>
+ <outputProperty>
+ <name>indent</name>
+ <value>no</value>
+ </outputProperty>
+ </outputProperties>
+ </transformationSet>
+ <transformationSet>
+ <dir>${project.build.directory}/downloads</dir>
+ <includes>Opc.Ua.Types.bsd</includes>
+ <stylesheet>src/main/xslt/opc-status.xsl</stylesheet>
+ <outputDir>${project.build.outputDirectory}/protocols/opcua</outputDir>
+ <fileMappers>
+ <fileMapper implementation="org.codehaus.plexus.components.io.filemappers.MergeFileMapper">
+ <targetName>opc-status.mspec</targetName>
+ </fileMapper>
+ </fileMappers>
+ <parameters>
+ <parameter>
+ <name>statusCodes</name>
+ <value>${project.build.directory}/downloads/StatusCode.csv</value>
+ </parameter>
+ </parameters>
+ <outputProperties>
+ <outputProperty>
+ <name>indent</name>
+ <value>no</value>
+ </outputProperty>
+ </outputProperties>
+ </transformationSet>
+ <transformationSet>
+ <dir>${project.build.directory}/downloads</dir>
+ <includes>Opc.Ua.Types.bsd</includes>
+ <stylesheet>src/main/xslt/opc-manual.xsl</stylesheet>
+ <outputDir>${project.build.outputDirectory}/protocols/opcua</outputDir>
+ <fileMappers>
+ <fileMapper implementation="org.codehaus.plexus.components.io.filemappers.MergeFileMapper">
+ <targetName>opc-manual.mspec</targetName>
+ </fileMapper>
+ </fileMappers>
+ <parameters>
+ <parameter>
+ <name>services</name>
+ <value>${project.build.directory}/downloads/Opc.Ua.NodeSet2.Services.xml</value>
+ </parameter>
+ </parameters>
+ <outputProperties>
+ <outputProperty>
+ <name>indent</name>
+ <value>no</value>
+ </outputProperty>
+ </outputProperties>
+ </transformationSet>
+ <transformationSet>
+ <dir>${project.build.directory}/downloads</dir>
+ <includes>Opc.Ua.Types.bsd</includes>
+ <stylesheet>src/main/xslt/opc-types.xsl</stylesheet>
+ <outputDir>${project.build.outputDirectory}/protocols/opcua</outputDir>
+ <fileMappers>
+ <fileMapper implementation="org.codehaus.plexus.components.io.filemappers.MergeFileMapper">
+ <targetName>opc-types.mspec</targetName>
+ </fileMapper>
+ </fileMappers>
+ <outputProperties>
+ <outputProperty>
+ <name>indent</name>
+ <value>no</value>
+ </outputProperty>
+ </outputProperties>
+ </transformationSet>
+ </transformationSets>
+ </configuration>
+ <dependencies>
+ <!-- https://mvnrepository.com/artifact/net.sf.saxon/Saxon-HE -->
+ <dependency>
+ <groupId>net.sf.saxon</groupId>
+ <artifactId>Saxon-HE</artifactId>
+ <version>10.5</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.plc4x</groupId>
+ <artifactId>plc4x-build-utils-language-base-freemarker</artifactId>
+ <version>0.9.0-SNAPSHOT</version>
+ </dependency>
+ </dependencies>
+ </plugin>
+
+ </plugins>
+ </build>
<dependencies>
<dependency>
<groupId>org.apache.plc4x</groupId>
<artifactId>plc4x-code-generation-protocol-base-mspec</artifactId>
<version>0.9.0-SNAPSHOT</version>
</dependency>
+
</dependencies>
</project>
diff --git a/protocols/opcua/src/main/java/org/apache/plc4x/protocol/opcua/OpcuaProtocol.java b/protocols/opcua/src/main/java/org/apache/plc4x/protocol/opcua/OpcuaProtocol.java
index f8fea64..911449c 100644
--- a/protocols/opcua/src/main/java/org/apache/plc4x/protocol/opcua/OpcuaProtocol.java
+++ b/protocols/opcua/src/main/java/org/apache/plc4x/protocol/opcua/OpcuaProtocol.java
@@ -25,6 +25,7 @@
import org.apache.plc4x.plugins.codegenerator.types.exceptions.GenerationException;
import java.io.InputStream;
+import java.util.LinkedHashMap;
import java.util.Map;
public class OpcuaProtocol implements Protocol {
@@ -36,11 +37,37 @@
@Override
public Map<String, TypeDefinition> getTypeDefinitions() throws GenerationException {
- InputStream schemaInputStream = OpcuaProtocol.class.getResourceAsStream("/protocols/opcua/opcua.mspec");
- if(schemaInputStream == null) {
+
+ InputStream manualInputStream = OpcuaProtocol.class.getResourceAsStream(
+ "/protocols/opcua/opc-manual.mspec");
+ if(manualInputStream == null) {
throw new GenerationException("Error loading message-format schema for protocol '" + getName() + "'");
}
- return new MessageFormatParser().parse(schemaInputStream);
- }
+ Map<String, TypeDefinition> typeDefinitionMap =
+ new LinkedHashMap<>(new MessageFormatParser().parse(manualInputStream));
+ InputStream servicesInputStream = OpcuaProtocol.class.getResourceAsStream(
+ "/protocols/opcua/opc-services.mspec");
+ if(servicesInputStream == null) {
+ throw new GenerationException("Error loading message-format schema for protocol '" + getName() + "'");
+ }
+ typeDefinitionMap.putAll(new MessageFormatParser().parse(servicesInputStream));
+
+
+ InputStream statusInputStream = OpcuaProtocol.class.getResourceAsStream(
+ "/protocols/opcua/opc-status.mspec");
+ if(statusInputStream == null) {
+ throw new GenerationException("Error loading message-format schema for protocol '" + getName() + "'");
+ }
+ typeDefinitionMap.putAll(new MessageFormatParser().parse(statusInputStream));
+
+
+ InputStream typesInputStream = OpcuaProtocol.class.getResourceAsStream(
+ "/protocols/opcua/opc-types.mspec");
+ if(typesInputStream == null) {
+ throw new GenerationException("Error loading message-format schema for protocol '" + getName() + "'");
+ }
+ typeDefinitionMap.putAll(new MessageFormatParser().parse(typesInputStream));
+ return typeDefinitionMap;
+ }
}
diff --git a/protocols/opcua/src/main/xslt/opc-common.xsl b/protocols/opcua/src/main/xslt/opc-common.xsl
new file mode 100644
index 0000000..5fc5633
--- /dev/null
+++ b/protocols/opcua/src/main/xslt/opc-common.xsl
@@ -0,0 +1,500 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<xsl:stylesheet version="2.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ xmlns:opc="http://opcfoundation.org/BinarySchema/"
+ xmlns:plc4x="https://plc4x.apache.org/"
+ xmlns:map="http://www.w3.org/2005/xpath-functions/map"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:ua="http://opcfoundation.org/UA/"
+ xmlns:tns="http://opcfoundation.org/UA/"
+ xmlns:node="http://opcfoundation.org/UA/2011/03/UANodeSet.xsd">
+
+ <xsl:output
+ method="text"
+ indent="no"
+ encoding="utf-8"
+ />
+
+ <xsl:param name="services"></xsl:param>
+ <xsl:param name="file" select="document($services)"/>
+
+ <xsl:variable name="originaldoc" select="/"/>
+
+ <xsl:variable name="lowercase" select="'abcdefghijklmnopqrstuvwxyz'"/>
+ <xsl:variable name="uppercase" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
+
+ <xsl:variable name="dataTypeLength" as="map(xs:string, xs:int)">
+ <xsl:map>
+ <xsl:for-each select="//opc:EnumeratedType">
+ <xsl:choose>
+ <xsl:when test="@Name != '' or @LengthInBits != ''">
+ <xsl:map-entry key="concat('ua:', xs:string(@Name))" select="xs:int(@LengthInBits)"/>
+ </xsl:when>
+ </xsl:choose>
+ </xsl:for-each>
+ </xsl:map>
+ </xsl:variable>
+
+ <xsl:template match="node:UADataType">
+ <xsl:variable name="browseName">
+ <xsl:value-of select='@BrowseName'/>
+ </xsl:variable>
+ <xsl:variable name="objectTypeId">
+ <xsl:call-template name="clean-datatype-string">
+ <xsl:with-param name="text" select="@BrowseName"/>
+ </xsl:call-template>
+ </xsl:variable>
+ <xsl:choose>
+ <xsl:when test="$originaldoc/opc:TypeDictionary/opc:StructuredType[@Name=$browseName] != ''"><xsl:text>
+ </xsl:text>['<xsl:value-of select="number(substring(@NodeId,3)) + 2"/><xsl:text>' </xsl:text><xsl:value-of select='$objectTypeId'/><xsl:text>
+ </xsl:text>
+ <xsl:message><xsl:value-of select="serialize($originaldoc/opc:TypeDictionary/opc:StructuredType[@Name=$browseName])"/></xsl:message>
+ <xsl:call-template name="plc4x:parseFields">
+ <xsl:with-param name="baseNode" select="$originaldoc/opc:TypeDictionary/opc:StructuredType[@Name=$browseName]"/>
+ <xsl:with-param name="currentNodePosition">1</xsl:with-param>
+ <xsl:with-param name="currentBytePosition">0</xsl:with-param>
+ <xsl:with-param name="currentBitPosition">0</xsl:with-param>
+ </xsl:call-template><xsl:text>
+ ]</xsl:text>
+ </xsl:when>
+ </xsl:choose>
+ </xsl:template>
+
+ <xsl:template match="node:UAVariable">
+ <xsl:variable name="browseName">
+ <xsl:value-of select='@BrowseName'/>
+ </xsl:variable>
+ <xsl:choose>
+ <xsl:when test="$originaldoc/opc:TypeDictionary/opc:StructuredType[@Name=$browseName]">
+ <xsl:choose>
+ <xsl:when test="not(@BrowseName='Vector') and not(substring(@BrowseName,1,1) = '<') and not(number(substring(@BrowseName,1,1)))">
+ [type '<xsl:value-of select='@BrowseName'/>'
+ <xsl:apply-templates select="$originaldoc/opc:TypeDictionary/opc:StructuredType[@Name=$browseName]"/>]
+ </xsl:when>
+ </xsl:choose>
+ </xsl:when>
+ </xsl:choose>
+ </xsl:template>
+
+ <xsl:template match="opc:EnumeratedType">
+ <xsl:message>[INFO] Parsing Enumerated Datatype - <xsl:value-of select="@Name"/></xsl:message><xsl:text>
+</xsl:text>[enum uint <xsl:value-of select="@LengthInBits"/> '<xsl:value-of select="@Name"/>'<xsl:text>
+</xsl:text>
+ <xsl:apply-templates select="opc:Documentation"/><xsl:text>
+ </xsl:text>
+ <xsl:apply-templates select="opc:EnumeratedValue"/>
+]
+ </xsl:template>
+
+ <xsl:template match="opc:Documentation">// <xsl:value-of select="."/></xsl:template>
+
+ <xsl:template match="opc:EnumeratedValue">
+ <xsl:message>[INFO] Parsing Enumerated Value - <xsl:value-of select="@Name"/></xsl:message>
+ <xsl:variable name="objectTypeId">
+ <xsl:call-template name="clean-id-string">
+ <xsl:with-param name="text" select="@Name"/>
+ <xsl:with-param name="switchField" select="../@Name"/>
+ <xsl:with-param name="switchValue" select="1"/>
+ </xsl:call-template>
+ </xsl:variable>['<xsl:value-of select="@Value"/>' <xsl:value-of select="$objectTypeId"/>]
+ </xsl:template>
+
+ <xsl:template match="opc:OpaqueType[not(@Name = 'Duration')]">
+ <xsl:message>[INFO] Parsing Opaque Datatype - <xsl:value-of select="@Name"/></xsl:message>
+ <xsl:variable name="objectTypeId">
+ <xsl:call-template name="clean-id-string">
+ <xsl:with-param name="text" select="@Name"/>
+ <xsl:with-param name="switchField" select="@SwitchField"/>
+ <xsl:with-param name="switchValue" select="@SwitchValue"/>
+ </xsl:call-template>
+ </xsl:variable>[type '<xsl:value-of select="@Name"/>'<xsl:text>
+ </xsl:text>
+ <xsl:apply-templates select="opc:Documentation"/>
+ <xsl:choose>
+ <xsl:when test="@LengthInBits != ''">
+ [simple uint <xsl:value-of select="@LengthInBits"/> '<xsl:value-of select="$objectTypeId"/>']</xsl:when>
+ </xsl:choose>
+]
+ </xsl:template>
+
+ <xsl:template match="opc:StructuredType[starts-with(@BaseType, 'tns:')]">
+ <xsl:message>[INFO] Parsing Structured Datatype - <xsl:value-of select="@Name"/></xsl:message>
+ <xsl:variable name="objectTypeId">
+ <xsl:call-template name="clean-datatype-string">
+ <xsl:with-param name="text" select="@Name"/>
+ </xsl:call-template>
+ </xsl:variable>[type '<xsl:value-of select="$objectTypeId"/>'<xsl:text>
+ </xsl:text>
+ <xsl:apply-templates select="opc:Documentation"/><xsl:text>
+ </xsl:text>
+ <xsl:call-template name="plc4x:parseFields">
+ <xsl:with-param name="baseNode" select="."/>
+ <xsl:with-param name="currentNodePosition">1</xsl:with-param>
+ <xsl:with-param name="currentBytePosition">0</xsl:with-param>
+ <xsl:with-param name="currentBitPosition">0</xsl:with-param>
+ </xsl:call-template>
+ ]
+ </xsl:template>
+
+ <xsl:template match="opc:StructuredType[not (@BaseType)]">
+ <xsl:message>[INFO] Parsing Structured Datatype - <xsl:value-of select="@Name"/></xsl:message>
+ <xsl:variable name="objectTypeId">
+ <xsl:call-template name="clean-datatype-string">
+ <xsl:with-param name="text" select="@Name"/>
+ </xsl:call-template>
+ </xsl:variable>[type '<xsl:value-of select="$objectTypeId"/>'<xsl:text>
+ </xsl:text>
+ <xsl:apply-templates select="opc:Documentation"/><xsl:text>
+ </xsl:text>
+ <xsl:call-template name="plc4x:parseFields">
+ <xsl:with-param name="baseNode" select="."/>
+ <xsl:with-param name="currentNodePosition">1</xsl:with-param>
+ <xsl:with-param name="currentBytePosition">0</xsl:with-param>
+ <xsl:with-param name="currentBitPosition">0</xsl:with-param>
+ </xsl:call-template>
+]
+ </xsl:template>
+
+ <xsl:template match="opc:Field">
+ <xsl:message>[INFO] Parsing Field - <xsl:value-of select="@Name"/></xsl:message>
+ <xsl:variable name="objectTypeId">
+ <xsl:value-of select="@Name"/>
+ </xsl:variable>
+ <xsl:variable name="lowerCaseName">
+ <xsl:call-template name="clean-id-string">
+ <xsl:with-param name="text" select="@Name"/>
+ <xsl:with-param name="switchField" select="@SwitchField"/>
+ <xsl:with-param name="switchValue" select="@SwitchValue"/>
+ </xsl:call-template>
+ </xsl:variable>
+ <xsl:variable name="lowerCaseLengthField">
+ <xsl:call-template name="lowerCaseLeadingChar">
+ <xsl:with-param name="text" select="@LengthField"/>
+ </xsl:call-template>
+ </xsl:variable>
+ <xsl:variable name="dataType">
+ <xsl:call-template name="plc4x:getDataTypeField">
+ <xsl:with-param name="datatype" select="@TypeName"/>
+ <xsl:with-param name="name" select="-1"/>
+ </xsl:call-template>
+ </xsl:variable>
+ <xsl:variable name="dataTypeLength"><xsl:value-of select="@Length"/></xsl:variable>
+ <xsl:variable name="mspecType">
+ <xsl:call-template name="plc4x:getMspecName">
+ <xsl:with-param name="datatype" select="@TypeName"/>
+ <xsl:with-param name="name" select="$lowerCaseName"/>
+ <xsl:with-param name="switchField" select="@SwitchField"/>
+ </xsl:call-template>
+ </xsl:variable>
+ <xsl:variable name="lowerCaseSwitchField">
+ <xsl:call-template name="clean-id-string">
+ <xsl:with-param name="text" select="@SwitchField"/>
+ <xsl:with-param name="switchField" select="@SwitchField"/>
+ <xsl:with-param name="switchValue" select="@SwitchValue"/>
+ </xsl:call-template>
+ </xsl:variable>
+ <!-- Depending on what kind of mspec variable it is, we have to include different arguments -->
+ <xsl:choose>
+ <xsl:when test="@LengthField">
+ <xsl:choose>
+ <xsl:when test="$dataType = 'ExtensionObjectDefinition'">
+ <xsl:variable name="browseName" select="substring-after(@TypeName,':')"/>
+ <xsl:variable name="id" select="number(substring-after($file/node:UANodeSet/node:UADataType[@BrowseName=$browseName]/@NodeId, '=')) + 2"/><xsl:text>
+ </xsl:text>[array <xsl:value-of select="$dataType"/> '<xsl:value-of select="$lowerCaseName"/>' count '<xsl:value-of select="$lowerCaseLengthField"/>' ['<xsl:value-of select='$id'/>']]
+ </xsl:when>
+ <xsl:when test="$dataType = 'ExtensionObject'">[array <xsl:value-of select="$dataType"/> '<xsl:value-of select="$lowerCaseName"/>' count '<xsl:value-of select="$lowerCaseLengthField"/>' ['true']]
+ </xsl:when>
+ <xsl:otherwise>[array <xsl:value-of select="$dataType"/> '<xsl:value-of select="$lowerCaseName"/>' count '<xsl:value-of select="$lowerCaseLengthField"/>']
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:when>
+ <xsl:when test="$mspecType = 'reserved'">
+ <xsl:choose>
+ <xsl:when test="xs:int(@Length) gt 1">[<xsl:value-of select="$mspecType"/><xsl:text> </xsl:text>uint <xsl:value-of select="$dataTypeLength"/> '0x00']<xsl:text>
+ </xsl:text>
+ </xsl:when>
+ <xsl:otherwise>[<xsl:value-of select="$mspecType"/><xsl:text> </xsl:text><xsl:value-of select="$dataType"/> 'false']<xsl:text>
+ </xsl:text>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:when>
+ <xsl:when test="$mspecType = 'optional'">[<xsl:value-of select="$mspecType"/><xsl:text> </xsl:text><xsl:value-of select="$dataType"/> '<xsl:value-of select="$lowerCaseName"/>' '<xsl:value-of select="$lowerCaseSwitchField"/>']
+ </xsl:when>
+ <xsl:when test="$dataType = 'ExtensionObjectDefinition'">
+ <xsl:variable name="browseName" select="substring-after(@TypeName,':')"/>
+ <xsl:variable name="id" select="number(substring-after($file/node:UANodeSet/node:UADataType[@BrowseName=$browseName]/@NodeId, '=')) + 2"/><xsl:text>
+ </xsl:text>[<xsl:value-of select="$mspecType"/><xsl:text> </xsl:text><xsl:value-of select="$dataType"/> '<xsl:value-of select="$lowerCaseName"/>' ['<xsl:value-of select='$id'/>']]
+ </xsl:when>
+ <xsl:when test="$dataType = 'ExtensionObject'">[<xsl:value-of select="$mspecType"/><xsl:text> </xsl:text><xsl:value-of select="$dataType"/> '<xsl:value-of select="$lowerCaseName"/>' ['true']]
+ </xsl:when>
+ <xsl:otherwise>[<xsl:value-of select="$mspecType"/><xsl:text> </xsl:text><xsl:value-of select="$dataType"/> '<xsl:value-of select="$lowerCaseName"/>']
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:template>
+
+ <!-- Get the Mspec type simple/reserved/implicit/virtual/etc... -->
+ <xsl:template name="plc4x:getMspecName">
+ <xsl:param name="datatype"/>
+ <xsl:param name="name"/>
+ <xsl:param name="switchField"/>
+ <xsl:message>[INFO] Getting Mspec type for <xsl:value-of select="$name"/>></xsl:message>
+ <xsl:choose>
+ <xsl:when test="starts-with($name, 'reserved')">reserved</xsl:when>
+ <xsl:when test="$switchField != ''">optional</xsl:when>
+ <xsl:otherwise>simple</xsl:otherwise>
+ </xsl:choose>
+ </xsl:template>
+
+ <!-- Convert a Data Type name so that it doesn't clash with mspec key words -->
+ <xsl:template name="clean-datatype-string">
+ <xsl:param name="text"/>
+ <xsl:choose>
+ <xsl:when test="$text = 'Vector'">OpcuaVector</xsl:when>
+ <xsl:when test="$text = 'Vector'">OpcuaVector</xsl:when>
+ <xsl:otherwise><xsl:value-of select="$text"/></xsl:otherwise>
+ </xsl:choose>
+ </xsl:template>
+
+ <!-- Convert a variable name so that it doesn't clash with mspec key words -->
+ <xsl:template name="clean-id-string">
+ <xsl:param name="text"/>
+ <xsl:param name="switchField"/>
+ <xsl:param name="switchValue"/>
+ <xsl:choose>
+ <xsl:when test="$switchValue">
+ <xsl:call-template name="lowerCaseLeadingChar">
+ <xsl:with-param name="text" select="concat($switchField, $text)"/>
+ </xsl:call-template>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:call-template name="lowerCaseLeadingChar">
+ <xsl:with-param name="text" select="$text"/>
+ </xsl:call-template>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:template>
+
+ <!-- Change the first character in string to lower case -->
+ <xsl:template name="lowerCaseLeadingChar">
+ <xsl:param name="text"/>
+ <xsl:value-of select="concat(translate(substring($text, 1, 1), $uppercase, $lowercase), substring($text, 2))"/>
+ </xsl:template>
+
+ <!-- Convert the OPCUA data types to mspec data types. -->
+ <xsl:template name="plc4x:getDataTypeField">
+ <xsl:param name="datatype"/>
+ <xsl:param name="name"/>
+ <xsl:choose>
+ <xsl:when test="$datatype = 'opc:Bit'">bit</xsl:when>
+ <xsl:when test="$datatype = 'opc:Boolean'">bit</xsl:when>
+ <xsl:when test="$datatype = 'opc:Byte'">uint 8</xsl:when>
+ <xsl:when test="$datatype = 'opc:SByte'">int 8</xsl:when>
+ <xsl:when test="$datatype = 'opc:Int16'">int 16</xsl:when>
+ <xsl:when test="$datatype = 'opc:UInt16'">uint 16</xsl:when>
+ <xsl:when test="$datatype = 'opc:Int32'">int 32</xsl:when>
+ <xsl:when test="$datatype = 'opc:UInt32'">uint 32</xsl:when>
+ <xsl:when test="$datatype = 'opc:Int64'">int 64</xsl:when>
+ <xsl:when test="$datatype = 'opc:UInt64'">uint 64</xsl:when>
+ <xsl:when test="$datatype = 'opc:Float'">float 8.23</xsl:when>
+ <xsl:when test="$datatype = 'opc:Double'">float 11.52</xsl:when>
+ <xsl:when test="$datatype = 'opc:Char'">string '1'</xsl:when>
+ <xsl:when test="$datatype = 'opc:CharArray'">PascalString</xsl:when>
+ <xsl:when test="$datatype = 'opc:Guid'">GuidValue</xsl:when>
+ <xsl:when test="$datatype = 'opc:ByteString'">PascalByteString</xsl:when>
+ <xsl:when test="$datatype = 'opc:DateTime'">int 64</xsl:when>
+ <xsl:when test="$datatype = 'opc:String'">PascalString</xsl:when>
+ <xsl:when test="not(starts-with($datatype, 'opc:'))">
+ <xsl:variable name="parent" select="$originaldoc/opc:TypeDictionary/opc:StructuredType[@Name=substring-after($datatype,':')]/@BaseType"/>
+ <xsl:choose>
+ <xsl:when test="$parent != ''">
+ <xsl:variable name="id" select="substring-after($file/node:UANodeSet/node:UADataType[@BrowseName=substring-after($datatype,':')]/@NodeId, ':')"/>
+ <xsl:choose>
+ <xsl:when test="substring-after($parent,':') = 'ExtensionObject'">ExtensionObjectDefinition</xsl:when>
+ <xsl:otherwise><xsl:value-of select="substring-after($parent,':')"/></xsl:otherwise>
+ </xsl:choose>
+ </xsl:when>
+ <xsl:otherwise><xsl:value-of select="substring-after($datatype,':')"/></xsl:otherwise>
+ </xsl:choose>
+ </xsl:when>
+ <xsl:otherwise><xsl:value-of select="substring-after($datatype,':')"/></xsl:otherwise>
+ </xsl:choose>
+ </xsl:template>
+
+ <!-- Gets the length in bits of a data type -->
+ <xsl:function name="plc4x:getDataTypeLength" as="xs:integer">
+ <xsl:param name="lengthMap" as="map(xs:string, xs:int)"/>
+ <xsl:param name="datatype"/>
+ <xsl:message>[DEBUG] Getting length of <xsl:value-of select="xs:string($datatype/[@TypeName])"/></xsl:message>
+ <xsl:choose>
+ <xsl:when test="map:contains($lengthMap, xs:string($datatype/[@TypeName]))">
+ <xsl:message>[DEBUG] Bit Length <xsl:value-of select="$lengthMap(xs:string($datatype/[@TypeName]))"/></xsl:message>
+ <xsl:value-of select="map:get($lengthMap, xs:string($datatype/[@TypeName]))"/>
+ </xsl:when>
+ <xsl:when test="($datatype/[@TypeName] = 'opc:Bit') or ($datatype/[@TypeName] = 'opc:Boolean')">
+ <xsl:choose>
+ <xsl:when test="$datatype/[@Length] != ''">
+ <xsl:value-of select="xs:int($datatype/[@Length])"/>
+ </xsl:when>
+ <xsl:otherwise>1</xsl:otherwise>
+ </xsl:choose>
+ </xsl:when>
+ <xsl:otherwise>8</xsl:otherwise>
+ </xsl:choose>
+ </xsl:function>
+
+ <!-- Parse the fields for each type, rearranging all of the bit based fields so their order matches that of the PLC4X mspec -->
+ <xsl:template name="plc4x:parseFields">
+ <xsl:param name="baseNode"/>
+ <xsl:param name="currentNodePosition" as="xs:int"/>
+ <xsl:param name="currentBitPosition" as="xs:int"/>
+ <xsl:param name="currentBytePosition" as="xs:int"/>
+ <xsl:message>[DEBUG] Recursively rearranging bit order in nodes, Position - <xsl:value-of select="$currentNodePosition"/>, Bit Position - <xsl:value-of select="$currentBitPosition"/>, Byte Position - <xsl:value-of select="$currentBytePosition"/></xsl:message>
+ <xsl:for-each select="$baseNode/opc:Field">
+ <xsl:message>[DEBUG] <xsl:value-of select="position()"/> - <xsl:value-of select="@TypeName"/></xsl:message>
+ </xsl:for-each>
+ <xsl:choose>
+ <xsl:when test="$currentNodePosition > count($baseNode/opc:Field)">
+ <xsl:message>Node Position - <xsl:value-of select="$currentNodePosition"/></xsl:message>
+ <xsl:message>Bit Position - <xsl:value-of select="$currentBitPosition"/></xsl:message>
+ <xsl:choose>
+ <xsl:when test="$currentBitPosition != 0">
+ <!-- Add a reserved field if we are halfway through a Byte. -->
+ <xsl:message>[DEBUG] Adding a reserved field</xsl:message>
+ <xsl:call-template name="plc4x:parseFields">
+ <xsl:with-param name="baseNode">
+ <xsl:copy-of select="$baseNode/opc:Field[position() lt ($currentNodePosition - $currentBytePosition)]"/>
+ <xsl:element name="opc:Field">
+ <xsl:attribute name="Name">ReservedX</xsl:attribute>
+ <xsl:attribute name="TypeName">opc:Bit</xsl:attribute>
+ <xsl:attribute name="Length"><xsl:value-of select="8-$currentBitPosition"/></xsl:attribute>
+ </xsl:element>
+ <xsl:copy-of select="$baseNode/opc:Field[(position() gt ($currentNodePosition - $currentBytePosition - 1))]"/>
+ </xsl:with-param>
+ <xsl:with-param name="currentNodePosition">
+ <xsl:value-of select="$currentNodePosition + 2"/>
+ </xsl:with-param>
+ <xsl:with-param name="currentBitPosition">
+ <xsl:value-of select="0"/>
+ </xsl:with-param>
+ <xsl:with-param name="currentBytePosition">
+ <xsl:value-of select="0"/>
+ </xsl:with-param>
+ </xsl:call-template>
+ </xsl:when>
+ <xsl:otherwise>
+ <!-- Return the rearranged nodes -->
+ <xsl:apply-templates select="$baseNode/opc:Field"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:choose>
+ <xsl:when test="plc4x:getDataTypeLength($dataTypeLength, $baseNode/opc:Field[$currentNodePosition][@TypeName]) lt 8">
+ <xsl:choose>
+ <xsl:when test="$currentBitPosition=0">
+ <!-- Put node into current position -->
+ <xsl:message>[DEBUG] First Bit in Byte</xsl:message>
+ <xsl:call-template name="plc4x:parseFields">
+ <xsl:with-param name="baseNode">
+ <xsl:copy-of select="$baseNode/opc:Field"/>
+ </xsl:with-param>
+ <xsl:with-param name="currentNodePosition">
+ <xsl:value-of select="$currentNodePosition + 1"/>
+ </xsl:with-param>
+ <xsl:with-param name="currentBitPosition">
+ <xsl:value-of select="plc4x:getDataTypeLength($dataTypeLength, $baseNode/opc:Field[position() = $currentNodePosition][@TypeName]) + $currentBitPosition"/>
+ </xsl:with-param>
+ <xsl:with-param name="currentBytePosition">
+ <xsl:value-of select="$currentBytePosition + 1"/>
+ </xsl:with-param>
+ </xsl:call-template>
+ </xsl:when>
+ <xsl:otherwise>
+ <!-- Put node into correct position based on bit and byte position -->
+ <xsl:message>[DEBUG] Additional Bit in Byte</xsl:message>
+ <xsl:call-template name="plc4x:parseFields">
+ <xsl:with-param name="baseNode">
+ <xsl:copy-of select="$baseNode/opc:Field[position() lt ($currentNodePosition - $currentBytePosition)]"/>
+ <xsl:copy-of select="$baseNode/opc:Field[position() = $currentNodePosition]"/>
+ <xsl:copy-of select="$baseNode/opc:Field[(position() gt ($currentNodePosition - $currentBytePosition - 1)) and (position() lt ($currentNodePosition))]"/>
+ <xsl:copy-of select="$baseNode/opc:Field[position() gt $currentNodePosition]"/>
+ </xsl:with-param>
+ <xsl:with-param name="currentNodePosition">
+ <xsl:value-of select="$currentNodePosition + 1"/>
+ </xsl:with-param>
+ <xsl:with-param name="currentBitPosition">
+ <xsl:value-of select="plc4x:getDataTypeLength($dataTypeLength, $baseNode/opc:Field[position() = $currentNodePosition][@TypeName]) + $currentBitPosition"/>
+ </xsl:with-param>
+ <xsl:with-param name="currentBytePosition">
+ <xsl:value-of select="$currentBytePosition + 1"/>
+ </xsl:with-param>
+ </xsl:call-template>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:choose>
+ <xsl:when test="$currentBitPosition != 0 and $currentBitPosition lt 8">
+ <!-- Add a reserved field if we are halfway through a Byte. -->
+ <xsl:message>[DEBUG] Adding a reserved field</xsl:message>
+ <xsl:call-template name="plc4x:parseFields">
+ <xsl:with-param name="baseNode">
+ <xsl:copy-of select="$baseNode/opc:Field[position() lt ($currentNodePosition - $currentBytePosition)]"/>
+ <xsl:element name="opc:Field">
+ <xsl:attribute name="Name">ReservedX</xsl:attribute>
+ <xsl:attribute name="TypeName">opc:Bit</xsl:attribute>
+ <xsl:attribute name="Length"><xsl:value-of select="8-$currentBitPosition"/></xsl:attribute>
+ </xsl:element>
+ <xsl:copy-of select="$baseNode/opc:Field[(position() gt ($currentNodePosition - $currentBytePosition - 1))]"/>
+ </xsl:with-param>
+ <xsl:with-param name="currentNodePosition">
+ <xsl:value-of select="$currentNodePosition + 2"/>
+ </xsl:with-param>
+ <xsl:with-param name="currentBitPosition">
+ <xsl:value-of select="0"/>
+ </xsl:with-param>
+ <xsl:with-param name="currentBytePosition">
+ <xsl:value-of select="0"/>
+ </xsl:with-param>
+ </xsl:call-template>
+ </xsl:when>
+ <xsl:otherwise>
+ <!-- Put node into current position -->
+ <xsl:message>[DEBUG] not a bit data type, just leave it in it's place</xsl:message>
+ <xsl:call-template name="plc4x:parseFields">
+ <xsl:with-param name="baseNode">
+ <xsl:copy-of select="$baseNode/opc:Field"/>
+ </xsl:with-param>
+ <xsl:with-param name="currentNodePosition">
+ <xsl:value-of select="$currentNodePosition + 1"/>
+ </xsl:with-param>
+ <xsl:with-param name="currentBitPosition">0</xsl:with-param>
+ <xsl:with-param name="currentBytePosition">0</xsl:with-param>
+ </xsl:call-template>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:template>
+</xsl:stylesheet>
\ No newline at end of file
diff --git a/protocols/opcua/src/main/xslt/opc-manual.xsl b/protocols/opcua/src/main/xslt/opc-manual.xsl
new file mode 100644
index 0000000..fc0a293
--- /dev/null
+++ b/protocols/opcua/src/main/xslt/opc-manual.xsl
@@ -0,0 +1,436 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<xsl:stylesheet version="2.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ xmlns:opc="http://opcfoundation.org/BinarySchema/"
+ xmlns:plc4x="https://plc4x.apache.org/"
+ xmlns:map="http://www.w3.org/2005/xpath-functions/map"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:ua="http://opcfoundation.org/UA/"
+ xmlns:tns="http://opcfoundation.org/UA/"
+ xmlns:node="http://opcfoundation.org/UA/2011/03/UANodeSet.xsd">
+
+ <xsl:output
+ method="text"
+ indent="no"
+ encoding="utf-8"
+ />
+
+ <xsl:import href="opc-common.xsl"/>
+
+ <xsl:variable name="originaldoc" select="/"/>
+
+ <xsl:param name="services"></xsl:param>
+ <xsl:param name="file" select="document($services)"/>
+
+ <xsl:template match="/">
+// Remark: The different fields are encoded in Little-endian.
+
+[type 'OpcuaAPU' [bit 'response']
+ [simple MessagePDU 'message' ['response']]
+]
+
+[discriminatedType 'MessagePDU' [bit 'response']
+ [discriminator string '24' 'messageType']
+ [typeSwitch 'messageType','response'
+ ['HEL','false' OpcuaHelloRequest
+ [simple string '8' 'chunk']
+ [implicit int 32 'messageSize' 'lengthInBytes']
+ [simple int 32 'version']
+ [simple int 32 'receiveBufferSize']
+ [simple int 32 'sendBufferSize']
+ [simple int 32 'maxMessageSize']
+ [simple int 32 'maxChunkCount']
+ [simple PascalString 'endpoint']
+ ]
+ ['ACK','true' OpcuaAcknowledgeResponse
+ [simple string '8' 'chunk']
+ [implicit int 32 'messageSize' 'lengthInBytes']
+ [simple int 32 'version']
+ [simple int 32 'receiveBufferSize']
+ [simple int 32 'sendBufferSize']
+ [simple int 32 'maxMessageSize']
+ [simple int 32 'maxChunkCount']
+ ]
+ ['OPN','false' OpcuaOpenRequest
+ [simple string '8' 'chunk']
+ [implicit int 32 'messageSize' 'lengthInBytes']
+ [simple int 32 'secureChannelId']
+ [simple PascalString 'endpoint']
+ [simple PascalByteString 'senderCertificate']
+ [simple PascalByteString 'receiverCertificateThumbprint']
+ [simple int 32 'sequenceNumber']
+ [simple int 32 'requestId']
+ [array int 8 'message' count 'messageSize - (endpoint.stringLength == -1 ? 0 : endpoint.stringLength ) - (senderCertificate.stringLength == -1 ? 0 : senderCertificate.stringLength) - (receiverCertificateThumbprint.stringLength == -1 ? 0 : receiverCertificateThumbprint.stringLength) - 32']
+ ]
+ ['OPN','true' OpcuaOpenResponse
+ [simple string '8' 'chunk']
+ [implicit int 32 'messageSize' 'lengthInBytes']
+ [simple int 32 'secureChannelId']
+ [simple PascalString 'securityPolicyUri']
+ [simple PascalByteString 'senderCertificate']
+ [simple PascalByteString 'receiverCertificateThumbprint']
+ [simple int 32 'sequenceNumber']
+ [simple int 32 'requestId']
+ [array int 8 'message' count 'messageSize - (securityPolicyUri.stringLength == -1 ? 0 : securityPolicyUri.stringLength) - (senderCertificate.stringLength == -1 ? 0 : senderCertificate.stringLength) - (receiverCertificateThumbprint.stringLength == -1 ? 0 : receiverCertificateThumbprint.stringLength) - 32']
+ ]
+ ['CLO','false' OpcuaCloseRequest
+ [simple string '8' 'chunk']
+ [implicit int 32 'messageSize' 'lengthInBytes']
+ [simple int 32 'secureChannelId']
+ [simple int 32 'secureTokenId']
+ [simple int 32 'sequenceNumber']
+ [simple int 32 'requestId']
+ [simple ExtensionObject 'message' ['false']]
+ ]
+ ['MSG','false' OpcuaMessageRequest
+ [simple string '8' 'chunk']
+ [implicit int 32 'messageSize' 'lengthInBytes']
+ [simple int 32 'secureChannelId']
+ [simple int 32 'secureTokenId']
+ [simple int 32 'sequenceNumber']
+ [simple int 32 'requestId']
+ [array int 8 'message' count 'messageSize - 24']
+ ]
+ ['MSG','true' OpcuaMessageResponse
+ [simple string '8' 'chunk']
+ [implicit int 32 'messageSize' 'lengthInBytes']
+ [simple int 32 'secureChannelId']
+ [simple int 32 'secureTokenId']
+ [simple int 32 'sequenceNumber']
+ [simple int 32 'requestId']
+ [array int 8 'message' count 'messageSize - 24']
+ ]
+ ]
+]
+
+[type 'ByteStringArray'
+ [simple int 32 'arrayLength']
+ [array uint 8 'value' count 'arrayLength']
+]
+
+[type 'GuidValue'
+ [simple uint 32 'data1']
+ [simple uint 16 'data2']
+ [simple uint 16 'data3']
+ [array int 8 'data4' count '2']
+ [array int 8 'data5' count '6']
+]
+
+[type 'ExpandedNodeId'
+ [simple bit 'namespaceURISpecified']
+ [simple bit 'serverIndexSpecified']
+ [simple NodeIdTypeDefinition 'nodeId']
+ [virtual string '-1' 'utf-8' 'identifier' 'nodeId.identifier']
+ [optional PascalString 'namespaceURI' 'namespaceURISpecified']
+ [optional uint 32 'serverIndex' 'serverIndexSpecified']
+]
+
+[type 'ExtensionHeader'
+ [reserved int 5 '0x00']
+ [simple bit 'xmlbody']
+ [simple bit 'binaryBody]
+]
+
+[type 'ExtensionObjectEncodingMask'
+ [reserved int 5 '0x00']
+ [simple bit 'typeIdSpecified']
+ [simple bit 'xmlbody']
+ [simple bit 'binaryBody]
+]
+
+[type 'ExtensionObject' [bit 'includeEncodingMask']
+ //A serialized object prefixed with its data type identifier.
+ [simple ExpandedNodeId 'typeId']
+ [optional ExtensionObjectEncodingMask 'encodingMask' 'includeEncodingMask']
+ [virtual string '-1' 'identifier' 'typeId.identifier']
+ [simple ExtensionObjectDefinition 'body' ['identifier']]
+]
+
+[discriminatedType 'ExtensionObjectDefinition' [string '-1' 'identifier']
+ [typeSwitch 'identifier'
+ ['0' NullExtension
+ ]
+
+ <xsl:for-each select="/opc:TypeDictionary/opc:StructuredType[(@BaseType = 'ua:ExtensionObject') and not(@Name = 'UserIdentityToken') and not(@Name = 'PublishedDataSetDataType') and not(@Name = 'DataSetReaderDataType')]">
+ <xsl:message><xsl:value-of select="@Name"/></xsl:message>
+ <xsl:variable name="extensionName" select="@Name"/>
+ <xsl:apply-templates select="$file/node:UANodeSet/node:UADataType[@BrowseName=$extensionName]"/>
+ </xsl:for-each>
+
+ ['811' DataChangeNotification
+ [implicit int 32 'notificationLength' 'lengthInBytes']
+ [simple int 32 'noOfMonitoredItems']
+ [array ExtensionObjectDefinition 'monitoredItems' count 'noOfMonitoredItems' ['808']]
+ [simple int 32 'noOfDiagnosticInfos']
+ [array DiagnosticInfo 'diagnosticInfos' count 'noOfDiagnosticInfos']
+ ]
+ ['916' EventNotificationList
+ [implicit int 32 'notificationLength' 'lengthInBytes']
+ [simple int 32 'noOfEvents']
+ [array ExtensionObjectDefinition 'events' count 'noOfEvents' ['919']]
+ ]
+ ['820' StatusChangeNotification
+ [implicit int 32 'notificationLength' 'lengthInBytes']
+ [simple StatusCode 'status']
+ [simple DiagnosticInfo 'diagnosticInfo']
+ ]
+
+ ['316' UserIdentityToken
+ [implicit int 32 'policyLength' 'policyId.lengthInBytes']
+ [simple PascalString 'policyId']
+ [simple UserIdentityTokenDefinition 'userIdentityTokenDefinition' ['policyId.stringValue']]
+ ]
+ ]
+]
+
+[discriminatedType 'UserIdentityTokenDefinition' [string '-1' 'identifier']
+ [typeSwitch 'identifier'
+ ['anonymous' AnonymousIdentityToken
+ ]
+ ['username' UserNameIdentityToken
+ [simple PascalString 'userName']
+ [simple PascalByteString 'password']
+ [simple PascalString 'encryptionAlgorithm']
+ ]
+ ['certificate' X509IdentityToken
+ [simple PascalByteString 'certificateData']
+ ]
+ ['identity' IssuedIdentityToken
+ [simple PascalByteString 'tokenData']
+ [simple PascalString 'encryptionAlgorithm']
+ ]
+ ]
+]
+
+
+[discriminatedType 'Variant'
+ [simple bit 'arrayLengthSpecified']
+ [simple bit 'arrayDimensionsSpecified']
+ [discriminator uint 6 'VariantType']
+ [typeSwitch 'VariantType','arrayLengthSpecified'
+ ['1' VariantBoolean [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array int 8 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['2' VariantSByte [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array int 8 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['3' VariantByte [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array uint 8 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['4' VariantInt16 [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array int 16 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['5' VariantUInt16 [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array uint 16 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['6' VariantInt32 [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array int 32 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['7' VariantUInt32 [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array uint 32 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['8' VariantInt64 [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array int 64 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['9' VariantUInt64 [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array uint 64 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['10' VariantFloat [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array float 8.23 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['11' VariantDouble [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array float 11.52 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['12' VariantString [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array PascalString 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['13' VariantDateTime [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array int 64 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['14' VariantGuid [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array GuidValue 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['15' VariantByteString [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array ByteStringArray 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['16' VariantXmlElement [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array PascalString 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['17' VariantNodeId [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array NodeId 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['18' VariantExpandedNodeId [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array ExpandedNodeId 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['19' VariantStatusCode [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array StatusCode 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['20' VariantQualifiedName [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array QualifiedName 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['21' VariantLocalizedText [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array LocalizedText 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['22' VariantExtensionObject [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array ExtensionObject 'value' count 'arrayLength == null ? 1 : arrayLength' ['true']]
+ ]
+ ['23' VariantDataValue [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array DataValue 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['24' VariantVariant [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array Variant 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ['25' VariantDiagnosticInfo [bit 'arrayLengthSpecified']
+ [optional int 32 'arrayLength' 'arrayLengthSpecified']
+ [array DiagnosticInfo 'value' count 'arrayLength == null ? 1 : arrayLength']
+ ]
+ ]
+ [optional int 32 'noOfArrayDimensions' 'arrayDimensionsSpecified']
+ [array bit 'arrayDimensions' count 'noOfArrayDimensions == null ? 0 : noOfArrayDimensions']
+]
+
+[discriminatedType 'NodeIdTypeDefinition'
+ [abstract string '-1' 'identifier']
+ [discriminator NodeIdType 'nodeType']
+ [typeSwitch 'nodeType'
+ ['nodeIdTypeTwoByte' NodeIdTwoByte
+ [simple uint 8 'id']
+ [virtual string '-1' 'identifier' 'id']
+ ]
+ ['nodeIdTypeFourByte' NodeIdFourByte
+ [simple uint 8 'namespaceIndex']
+ [simple uint 16 'id']
+ [virtual string '-1' 'identifier' 'id']
+ ]
+ ['nodeIdTypeNumeric' NodeIdNumeric
+ [simple uint 16 'namespaceIndex']
+ [simple uint 32 'id']
+ [virtual string '-1' 'identifier' 'id']
+ ]
+ ['nodeIdTypeString' NodeIdString
+ [simple uint 16 'namespaceIndex']
+ [simple PascalString 'id']
+ [virtual string '-1' 'identifier' 'id.stringValue']
+ ]
+ ['nodeIdTypeGuid' NodeIdGuid
+ [simple uint 16 'namespaceIndex']
+ [array int 8 'id' count '16']
+ [virtual string '-1' 'identifier' 'id']
+ ]
+ ['nodeIdTypeByteString' NodeIdByteString
+ [simple uint 16 'namespaceIndex']
+ [simple PascalByteString 'id']
+ [virtual string '-1' 'identifier' 'id.stringValue']
+ ]
+ ]
+]
+
+[type 'NodeId'
+ [reserved int 2 '0x00']
+ [simple NodeIdTypeDefinition 'nodeId']
+ [virtual string '-1' 'id' 'nodeId.identifier']
+]
+
+[type 'PascalString'
+ [implicit int 32 'sLength' 'stringValue.length == 0 ? -1 : stringValue.length']
+ [simple string 'sLength == -1 ? 0 : sLength * 8' 'UTF-8' 'stringValue']
+ [virtual int 32 'stringLength' 'stringValue.length == -1 ? 0 : stringValue.length']
+]
+
+[type 'PascalByteString'
+ [simple int 32 'stringLength']
+ [array int 8 'stringValue' count 'stringLength == -1 ? 0 : stringLength' ]
+]
+
+[type 'Structure'
+
+]
+
+[type 'DataTypeDefinition'
+
+]
+
+<xsl:apply-templates select="/opc:TypeDictionary/opc:StructuredType[(@Name != 'ExtensionObject') and (@Name != 'Variant') and (@Name != 'NodeId') and (@Name != 'ExpandedNodeId') and not(@BaseType)]"/>
+<xsl:apply-templates select="/opc:TypeDictionary/opc:EnumeratedType"/>
+<xsl:apply-templates select="/opc:TypeDictionary/opc:OpaqueType"/>
+
+[enum string '-1' 'OpcuaDataType' [uint 8 'variantType']
+ ['NULL' NULL ['0']]
+ ['BOOL' BOOL ['1']]
+ ['BYTE' BYTE ['3']]
+ ['SINT' SINT ['2']]
+ ['INT' INT ['4']]
+ ['DINT' DINT ['6']]
+ ['LINT' LINT ['8']]
+ ['USINT' USINT ['3']]
+ ['UINT' UINT ['5']]
+ ['UDINT' UDINT ['7']]
+ ['ULINT' ULINT ['9']]
+ ['REAL' REAL ['10']]
+ ['LREAL' LREAL ['11']]
+ ['TIME' TIME ['1']]
+ ['LTIME' LTIME ['1']]
+ ['DATE' DATE ['1']]
+ ['LDATE' LDATE ['1']]
+ ['TIME_OF_DAY' TIME_OF_DAY ['1']]
+ ['LTIME_OF_DAY' LTIME_OF_DAY ['1']]
+ ['DATE_AND_TIME' DATE_AND_TIME ['13']]
+ ['LDATE_AND_TIME' LDATE_AND_TIME ['1']]
+ ['CHAR' CHAR ['1']]
+ ['WCHAR' WCHAR ['1']]
+ ['STRING' STRING ['12']]
+]
+
+[enum string '-1' 'OpcuaIdentifierType'
+ ['s' STRING_IDENTIFIER]
+ ['i' NUMBER_IDENTIFIER]
+ ['g' GUID_IDENTIFIER]
+ ['b' BINARY_IDENTIFIER]
+]
+
+
+ </xsl:template>
+</xsl:stylesheet>
diff --git a/protocols/opcua/src/main/xslt/opc-services.xsl b/protocols/opcua/src/main/xslt/opc-services.xsl
new file mode 100644
index 0000000..04fe8b7
--- /dev/null
+++ b/protocols/opcua/src/main/xslt/opc-services.xsl
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<xsl:stylesheet version="2.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ xmlns:opc="http://opcfoundation.org/BinarySchema/"
+ xmlns:plc4x="https://plc4x.apache.org/"
+ xmlns:map="http://www.w3.org/2005/xpath-functions/map"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:ua="http://opcfoundation.org/UA/"
+ xmlns:tns="http://opcfoundation.org/UA/"
+ xmlns:node="http://opcfoundation.org/UA/2011/03/UANodeSet.xsd">
+
+ <xsl:output
+ method="text"
+ indent="no"
+ encoding="utf-8"
+ />
+ <xsl:import href="opc-common.xsl"/>
+
+ <xsl:variable name="originaldoc" select="/"/>
+
+ <xsl:param name="servicesEnum"></xsl:param>
+
+ <xsl:param name="servicesEnumFile" select="unparsed-text($servicesEnum)"/>
+
+ <xsl:template match="/">
+ <xsl:call-template name="servicesEnumParsing"/>
+ </xsl:template>
+
+ <xsl:template name="servicesEnumParsing" >
+ <xsl:variable name="tokenizedLine" select="tokenize($servicesEnumFile, '\r\n|\r|\n')" />
+[enum int 32 'OpcuaNodeIdServices'<xsl:text>
+ </xsl:text>
+ <xsl:for-each select="$tokenizedLine">
+ <xsl:variable select="tokenize(., ',')" name="values" />
+ <xsl:choose>
+ <xsl:when test="$values[2]">['<xsl:value-of select="$values[2]"/>' <xsl:value-of select="$values[1]"/>]<xsl:text>
+ </xsl:text>
+ </xsl:when>
+ </xsl:choose>
+ </xsl:for-each>
+]
+ </xsl:template>
+</xsl:stylesheet>
\ No newline at end of file
diff --git a/protocols/opcua/src/main/xslt/opc-status.xsl b/protocols/opcua/src/main/xslt/opc-status.xsl
new file mode 100644
index 0000000..af9eac7
--- /dev/null
+++ b/protocols/opcua/src/main/xslt/opc-status.xsl
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<xsl:stylesheet version="2.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ xmlns:opc="http://opcfoundation.org/BinarySchema/"
+ xmlns:plc4x="https://plc4x.apache.org/"
+ xmlns:map="http://www.w3.org/2005/xpath-functions/map"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:ua="http://opcfoundation.org/UA/"
+ xmlns:tns="http://opcfoundation.org/UA/"
+ xmlns:node="http://opcfoundation.org/UA/2011/03/UANodeSet.xsd">
+
+ <xsl:output
+ method="text"
+ indent="no"
+ encoding="utf-8"
+ />
+ <xsl:import href="opc-common.xsl"/>
+
+ <xsl:variable name="originaldoc" select="/"/>
+
+ <xsl:param name="statusCodes"></xsl:param>
+
+ <xsl:param name="statusCodeFile" select="unparsed-text($statusCodes)"/>
+
+ <xsl:template match="/">
+ <xsl:call-template name="statusCodeParsing"/>
+ </xsl:template>
+
+ <xsl:template name="statusCodeParsing" >
+ <xsl:variable name="tokenizedLine" select="tokenize($statusCodeFile, '\r\n|\r|\n')" />
+[enum uint 32 'OpcuaStatusCode'<xsl:text>
+ </xsl:text>
+ <xsl:for-each select="$tokenizedLine">
+ <xsl:variable select="tokenize(., ',')" name="values" /> ['<xsl:value-of select="$values[2]"/>L' <xsl:value-of select="$values[1]"/>]<xsl:text>
+ </xsl:text>
+ </xsl:for-each>
+]
+ </xsl:template>
+</xsl:stylesheet>
\ No newline at end of file
diff --git a/protocols/opcua/src/main/xslt/opc-types.xsl b/protocols/opcua/src/main/xslt/opc-types.xsl
new file mode 100644
index 0000000..46c3f21
--- /dev/null
+++ b/protocols/opcua/src/main/xslt/opc-types.xsl
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<xsl:stylesheet version="2.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ xmlns:opc="http://opcfoundation.org/BinarySchema/"
+ xmlns:plc4x="https://plc4x.apache.org/"
+ xmlns:map="http://www.w3.org/2005/xpath-functions/map"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:ua="http://opcfoundation.org/UA/"
+ xmlns:tns="http://opcfoundation.org/UA/"
+ xmlns:node="http://opcfoundation.org/UA/2011/03/UANodeSet.xsd">
+
+ <xsl:output
+ method="text"
+ indent="no"
+ encoding="utf-8"
+ />
+
+ <xsl:import href="opc-common.xsl"/>
+
+ <xsl:variable name="originaldoc" select="/"/>
+
+ <xsl:variable name="dataTypeLength" as="map(xs:string, xs:int)">
+ <xsl:map>
+ <xsl:for-each select="//opc:EnumeratedType">
+ <xsl:choose>
+ <xsl:when test="@Name != '' or @LengthInBits != ''">
+ <xsl:map-entry key="concat('ua:', xs:string(@Name))" select="xs:int(@LengthInBits)"/>
+ </xsl:when>
+ </xsl:choose>
+ </xsl:for-each>
+ </xsl:map>
+ </xsl:variable>
+
+
+
+ <xsl:template match="/">
+
+ </xsl:template>
+</xsl:stylesheet>