This is an automated email from the ASF dual-hosted git repository.
chrisdutz pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git
The following commit(s) were added to refs/heads/develop by this push:
new d221862ef0 feat: Implemented correct array handling in OPC-UA also
added a lot of new namespace tests.
d221862ef0 is described below
commit d221862ef0635bb23c6b6811944252e2a4bf1f21
Author: Christofer Dutz <[email protected]>
AuthorDate: Tue Jul 14 19:25:34 2026 +0200
feat: Implemented correct array handling in OPC-UA also added a lot of new
namespace tests.
---
.../apache/plc4x/java/opcua/OpcuaConnection.java | 91 ++++----
.../org/apache/plc4x/java/opcua/tag/OpcuaTag.java | 90 +++++++-
.../plc4x/java/opcua/OpcuaPlcDriverTest.java | 174 +++++++++++++++
.../java/opcua/tag/OpcuaTagIndexRangeTest.java | 104 +++++++++
.../milo/examples/server/Plc4xTestNamespace.java | 241 +++++++++++++++++++++
.../milo/examples/server/TestMiloServer.java | 7 +
6 files changed, 664 insertions(+), 43 deletions(-)
diff --git
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaConnection.java
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaConnection.java
index 3caabb747d..07ac00e679 100644
---
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaConnection.java
+++
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaConnection.java
@@ -28,6 +28,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import org.apache.plc4x.java.api.authentication.PlcAuthentication;
+import org.apache.plc4x.java.api.authentication.PlcNullAuthentication;
import
org.apache.plc4x.java.api.authentication.PlcUsernamePasswordAuthentication;
import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
@@ -198,7 +199,9 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
// Authentication passed to getConnection(url, authentication) takes
precedence over
// credentials embedded in the connection string.
PlcAuthentication passed = getAuthentication();
- if (passed != null) {
+ // PlcNullAuthentication is the explicit "no credentials / anonymous"
marker — treat it the
+ // same as no authentication rather than an unsupported token type.
+ if (passed != null && !(passed instanceof PlcNullAuthentication)) {
return passed;
}
if (configuration.getUsername() != null && configuration.getPassword()
!= null) {
@@ -316,7 +319,7 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
readValueArray.add(new ReadValueId(nodeId,
tag.getAttributeId().getValue(),
- NULL_STRING,
+ indexRangeOf(tag),
new QualifiedName(0, NULL_STRING)));
}
@@ -333,6 +336,12 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
});
}
+ /** The tag's OPC UA IndexRange as a PascalString, or the null string when
the whole node is addressed. */
+ private static PascalString indexRangeOf(OpcuaTag tag) {
+ String indexRange = tag.getIndexRange();
+ return indexRange != null ? new PascalString(indexRange) : NULL_STRING;
+ }
+
public static NodeId generateNodeId(OpcuaTag tag) {
NodeId nodeId = null;
if (tag.getIdentifierType() == OpcuaIdentifierType.BINARY_IDENTIFIER) {
@@ -1327,6 +1336,10 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
}
}
int length = valueObject.getLength();
+ // When an IndexRange selects part of an array, the written value must
itself be an array
+ // matching the range — even a single selected element is a 1-element
array, not a scalar.
+ boolean arraySpecified = length > 1 || tag.getIndexRange() != null;
+ Integer arrayLength = arraySpecified ? length : null;
switch (dataType) {
// Simple boolean values
case BOOL:
@@ -1334,11 +1347,11 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
for (int i = 0; i < length; i++) {
tmpBOOL[i] = valueObject.getIndex(i).getByte();
}
- return new VariantBoolean(length != 1,
+ return new VariantBoolean(arraySpecified,
dimsSpec,
noOfDims,
arrayDims,
- length == 1 ? null : length,
+ arrayLength,
tmpBOOL);
// 8-Bit Bit-Strings (Groups of Boolean Values)
@@ -1347,11 +1360,11 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
for (int i = 0; i < length; i++) {
tmpBYTE.add(valueObject.getIndex(i).getShort());
}
- return new VariantByte(length != 1,
+ return new VariantByte(arraySpecified,
dimsSpec,
noOfDims,
arrayDims,
- length == 1 ? null : length,
+ arrayLength,
tmpBYTE);
// 16-Bit Bit-Strings (Groups of Boolean Values)
@@ -1360,11 +1373,11 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
for (int i = 0; i < length; i++) {
tmpWORD.add(valueObject.getIndex(i).getInteger());
}
- return new VariantUInt16(length != 1,
+ return new VariantUInt16(arraySpecified,
dimsSpec,
noOfDims,
arrayDims,
- length == 1 ? null : length,
+ arrayLength,
tmpWORD);
// 32-Bit Bit-Strings (Groups of Boolean Values)
@@ -1373,11 +1386,11 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
for (int i = 0; i < length; i++) {
tmpDWORD.add(valueObject.getIndex(i).getLong());
}
- return new VariantUInt32(length != 1,
+ return new VariantUInt32(arraySpecified,
dimsSpec,
noOfDims,
arrayDims,
- length == 1 ? null : length,
+ arrayLength,
tmpDWORD);
// 64-Bit Bit-Strings (Groups of Boolean Values)
@@ -1386,11 +1399,11 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
for (int i = 0; i < length; i++) {
tmpLWORD.add(valueObject.getIndex(i).getBigInteger());
}
- return new VariantUInt64(length != 1,
+ return new VariantUInt64(arraySpecified,
dimsSpec,
noOfDims,
arrayDims,
- length == 1 ? null : length,
+ arrayLength,
tmpLWORD);
// 8-Bit Unsigned Integers
@@ -1399,11 +1412,11 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
for (int i = 0; i < length; i++) {
tmpUSINT.add(valueObject.getIndex(i).getShort());
}
- return new VariantByte(length != 1,
+ return new VariantByte(arraySpecified,
dimsSpec,
noOfDims,
arrayDims,
- length == 1 ? null : length,
+ arrayLength,
tmpUSINT);
// 8-Bit Signed Integers
@@ -1412,11 +1425,11 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
for (int i = 0; i < length; i++) {
tmpSINT[i] = valueObject.getIndex(i).getByte();
}
- return new VariantSByte(length != 1,
+ return new VariantSByte(arraySpecified,
dimsSpec,
noOfDims,
arrayDims,
- length == 1 ? null : length,
+ arrayLength,
tmpSINT);
// 16-Bit Unsigned Integers
@@ -1425,11 +1438,11 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
for (int i = 0; i < length; i++) {
tmpUINT.add(valueObject.getIndex(i).getInt());
}
- return new VariantUInt16(length != 1,
+ return new VariantUInt16(arraySpecified,
dimsSpec,
noOfDims,
arrayDims,
- length == 1 ? null : length,
+ arrayLength,
tmpUINT);
// 16-Bit Signed Integers
@@ -1438,11 +1451,11 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
for (int i = 0; i < length; i++) {
tmpINT16.add(valueObject.getIndex(i).getShort());
}
- return new VariantInt16(length != 1,
+ return new VariantInt16(arraySpecified,
dimsSpec,
noOfDims,
arrayDims,
- length == 1 ? null : length,
+ arrayLength,
tmpINT16);
// 32-Bit Unsigned Integers
@@ -1451,11 +1464,11 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
for (int i = 0; i < length; i++) {
tmpUDINT.add(valueObject.getIndex(i).getLong());
}
- return new VariantUInt32(length != 1,
+ return new VariantUInt32(arraySpecified,
dimsSpec,
noOfDims,
arrayDims,
- length == 1 ? null : length,
+ arrayLength,
tmpUDINT);
// 32-Bit Signed Integers
@@ -1464,11 +1477,11 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
for (int i = 0; i < length; i++) {
tmpDINT.add(valueObject.getIndex(i).getInt());
}
- return new VariantInt32(length != 1,
+ return new VariantInt32(arraySpecified,
dimsSpec,
noOfDims,
arrayDims,
- length == 1 ? null : length,
+ arrayLength,
tmpDINT);
// 64-Bit Unsigned Integers
@@ -1477,11 +1490,11 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
for (int i = 0; i < length; i++) {
tmpULINT.add(valueObject.getIndex(i).getBigInteger());
}
- return new VariantUInt64(length != 1,
+ return new VariantUInt64(arraySpecified,
dimsSpec,
noOfDims,
arrayDims,
- length == 1 ? null : length,
+ arrayLength,
tmpULINT);
// 64-Bit Signed Integers
@@ -1490,11 +1503,11 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
for (int i = 0; i < length; i++) {
tmpLINT.add(valueObject.getIndex(i).getLong());
}
- return new VariantInt64(length != 1,
+ return new VariantInt64(arraySpecified,
dimsSpec,
noOfDims,
arrayDims,
- length == 1 ? null : length,
+ arrayLength,
tmpLINT);
// 32-Bit Floating Point Values
@@ -1503,11 +1516,11 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
for (int i = 0; i < length; i++) {
tmpREAL.add(valueObject.getIndex(i).getFloat());
}
- return new VariantFloat(length != 1,
+ return new VariantFloat(arraySpecified,
dimsSpec,
noOfDims,
arrayDims,
- length == 1 ? null : length,
+ arrayLength,
tmpREAL);
// 64-Bit Floating Point Values
@@ -1516,11 +1529,11 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
for (int i = 0; i < length; i++) {
tmpLREAL.add(valueObject.getIndex(i).getDouble());
}
- return new VariantDouble(length != 1,
+ return new VariantDouble(arraySpecified,
dimsSpec,
noOfDims,
arrayDims,
- length == 1 ? null : length,
+ arrayLength,
tmpLREAL);
// UTF-8 Characters and Strings
@@ -1535,11 +1548,11 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
String s = valueObject.getIndex(i).getString();
tmpString.add(new PascalString(s));
}
- return new VariantString(length != 1,
+ return new VariantString(arraySpecified,
dimsSpec,
noOfDims,
arrayDims,
- length == 1 ? null : length,
+ arrayLength,
tmpString);
case DATE_AND_TIME:
@@ -1547,11 +1560,11 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
for (int i = 0; i < length; i++) {
tmpDateTime.add(valueObject.getIndex(i).getDateTime().toEpochSecond(ZoneOffset.UTC));
}
- return new VariantDateTime(length != 1,
+ return new VariantDateTime(arraySpecified,
dimsSpec,
noOfDims,
arrayDims,
- length == 1 ? null : length,
+ arrayLength,
tmpDateTime);
// IEC 61131-3 TIME is modelled by S7-1500 OPC UA as a signed
@@ -1561,11 +1574,11 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
for (int i = 0; i < length; i++) {
tmpTime.add((int)
valueObject.getIndex(i).getDuration().toMillis());
}
- return new VariantInt32(length != 1,
+ return new VariantInt32(arraySpecified,
dimsSpec,
noOfDims,
arrayDims,
- length == 1 ? null : length,
+ arrayLength,
tmpTime);
default:
throw new PlcRuntimeException("Unsupported write tag type " +
dataType);
@@ -1590,7 +1603,7 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
writeValueList.add(new WriteValue(nodeId,
tag.getAttributeId().getValue(),
- NULL_STRING,
+ indexRangeOf(tag),
new DataValue(
false,
false,
diff --git
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaTag.java
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaTag.java
index 0981d92809..b4eecd894d 100644
---
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaTag.java
+++
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaTag.java
@@ -44,9 +44,21 @@ public class OpcuaTag implements PlcSubscriptionTag {
// Inline tag-config pattern that the old SPI's {@code TagConfigParser}
used
// to append; kept here so the address-string syntax stays compatible.
private static final String TAG_CONFIG_PATTERN =
"(\\|(?<config>(?:(?:[a-zA-Z\\-_]+=[a-zA-Z0-9\\-_]+)(?:,(?:[a-zA-Z\\-_]+=[a-zA-Z0-9\\-_]+))*)))?";
- private static final String OPC_UTA_TAG_ADDRESS =
"^ns=(?<namespace>\\d+);(?<identifierType>[isgb])=(?<identifier>[^;]+)?(;a=(?<attributeId>[^;]+))?(;(?<datatype>[a-zA-Z_]+))?";
+ // The identifier is any run of non-';' characters, except that a
bracketed segment '[...]' may
+ // itself contain ';' — this lets an array-index suffix carry a ';base'
(e.g. "Foo[3..8;1]")
+ // without the inner ';' being mistaken for the ';a='/';TYPE' delimiters
that follow.
+ private static final String OPC_UTA_TAG_ADDRESS =
"^ns=(?<namespace>\\d+);(?<identifierType>[isgb])=(?<identifier>(?:[^;\\[]|\\[[^\\]]*\\])+)?(;a=(?<attributeId>[^;]+))?(;(?<datatype>[a-zA-Z_]+))?";
public static final Pattern ADDRESS_PATTERN =
Pattern.compile(OPC_UTA_TAG_ADDRESS + TAG_CONFIG_PATTERN + "$");
+ // A trailing run of array-index brackets on the identifier, e.g. "[8]",
"[3..8]" or the
+ // multi-dimensional "[1..2][0..5;1]". Each bracket is a single index or
an inclusive "lo..hi"
+ // range, with an optional ";base" giving the array's lower bound (default
0). The grammar is
+ // strictly numeric so a normal string identifier that happens to contain
'[' is left untouched.
+ private static final Pattern INDEX_RANGE_PATTERN =
+ Pattern.compile("(\\[\\d+(?:\\.\\.\\d+)?(?:;\\d+)?\\])+$");
+ private static final Pattern SINGLE_BRACKET_PATTERN =
+ Pattern.compile("\\[(\\d+)(?:\\.\\.(\\d+))?(?:;(\\d+))?\\]");
+
private final OpcuaIdentifierType identifierType;
private final int namespace;
@@ -59,7 +71,20 @@ public class OpcuaTag implements PlcSubscriptionTag {
private final Map<String, String> config;
+ // The array-index expression exactly as written by the user (e.g.
"[3..8;1]"), or null. Kept
+ // for address round-tripping; the on-the-wire OPC UA IndexRange is
derived via #getIndexRange().
+ private final String indexRangeExpression;
+
+ // The resolved OPC UA IndexRange string (0-based, inclusive,
comma-separated per dimension,
+ // e.g. "2:7"), or null when the tag addresses the whole node.
+ private final String indexRange;
+
private OpcuaTag(Integer namespace, String identifier, OpcuaIdentifierType
identifierType, AttributeId attributeId, OpcuaDataType dataType, Map<String,
String> config) {
+ this(namespace, identifier, identifierType, attributeId, dataType,
config, null, null);
+ }
+
+ private OpcuaTag(Integer namespace, String identifier, OpcuaIdentifierType
identifierType, AttributeId attributeId,
+ OpcuaDataType dataType, Map<String, String> config,
String indexRangeExpression, String indexRange) {
this.identifier = Objects.requireNonNull(identifier);
this.identifierType = Objects.requireNonNull(identifierType);
this.namespace = namespace != null ? namespace : 0;
@@ -69,6 +94,8 @@ public class OpcuaTag implements PlcSubscriptionTag {
this.attributeId = attributeId;
this.dataType = dataType;
this.config = config;
+ this.indexRangeExpression = indexRangeExpression;
+ this.indexRange = indexRange;
}
public static OpcuaTag of(String address) {
@@ -78,6 +105,19 @@ public class OpcuaTag implements PlcSubscriptionTag {
}
String identifier = matcher.group("identifier");
+ // Split a trailing array-index expression (e.g. "...Int[3..8]") off
the identifier and
+ // translate it to an OPC UA IndexRange. Left untouched when there is
no such suffix.
+ String indexRangeExpression = null;
+ String indexRange = null;
+ if (identifier != null) {
+ Matcher indexMatcher = INDEX_RANGE_PATTERN.matcher(identifier);
+ if (indexMatcher.find()) {
+ indexRangeExpression = indexMatcher.group();
+ indexRange = toOpcuaIndexRange(address, indexRangeExpression);
+ identifier = identifier.substring(0, indexMatcher.start());
+ }
+ }
+
String identifierTypeString = matcher.group("identifierType");
OpcuaIdentifierType identifierType =
OpcuaIdentifierType.enumForValue(identifierTypeString);
@@ -99,7 +139,36 @@ public class OpcuaTag implements PlcSubscriptionTag {
attributeId = AttributeId.valueOf(attributeElement);
}
}
- return new OpcuaTag(namespace, identifier, identifierType,
attributeId, dataType, parseConfig(matcher.group("config")));
+ return new OpcuaTag(namespace, identifier, identifierType,
attributeId, dataType,
+ parseConfig(matcher.group("config")), indexRangeExpression,
indexRange);
+ }
+
+ /**
+ * Translates the user's array-index expression into an OPC UA IndexRange
string. Each bracket
+ * becomes one dimension: a single index {@code [n]} or an inclusive range
{@code [lo..hi]},
+ * with an optional {@code ;base} lower bound (default 0) subtracted so
the result is 0-based, as
+ * OPC UA requires. Dimensions are comma-separated. Example: {@code
[3..8;1]} -> {@code "2:7"}.
+ */
+ private static String toOpcuaIndexRange(String address, String
indexRangeExpression) {
+ StringBuilder result = new StringBuilder();
+ Matcher bracket = SINGLE_BRACKET_PATTERN.matcher(indexRangeExpression);
+ while (bracket.find()) {
+ long low = Long.parseLong(bracket.group(1));
+ long high = bracket.group(2) != null ?
Long.parseLong(bracket.group(2)) : low;
+ long base = bracket.group(3) != null ?
Long.parseLong(bracket.group(3)) : 0;
+ low -= base;
+ high -= base;
+ if (low < 0 || high < low) {
+ throw new PlcInvalidTagException("Invalid array index range '"
+ bracket.group()
+ + "' in tag '" + address + "': resolved to " + low + ".."
+ high
+ + " (indices must be non-negative and low <= high after
applying the base)");
+ }
+ if (result.length() > 0) {
+ result.append(',');
+ }
+ result.append(low == high ? Long.toString(low) : low + ":" + high);
+ }
+ return result.toString();
}
/** Parses the tag's config tail ({@code |k=v,k=v}) into a map. */
@@ -119,7 +188,8 @@ public class OpcuaTag implements PlcSubscriptionTag {
@Override
public PlcTag getTag() {
- return new OpcuaTag(namespace, identifier, identifierType,
attributeId, dataType, config);
+ return new OpcuaTag(namespace, identifier, identifierType,
attributeId, dataType, config,
+ indexRangeExpression, indexRange);
}
public static boolean matches(String address) {
@@ -146,6 +216,14 @@ public class OpcuaTag implements PlcSubscriptionTag {
return attributeId;
}
+ /**
+ * @return the resolved OPC UA IndexRange (0-based, inclusive,
comma-separated per dimension,
+ * e.g. {@code "2:7"}), or {@code null} when the whole node is
addressed.
+ */
+ public String getIndexRange() {
+ return indexRange;
+ }
+
public Map<String, String> getConfig() {
return config;
}
@@ -153,6 +231,9 @@ public class OpcuaTag implements PlcSubscriptionTag {
@Override
public String getAddressString() {
String address = String.format("ns=%d;%s=%s", namespace,
identifierType.getValue(), identifier);
+ if (indexRangeExpression != null) {
+ address += indexRangeExpression;
+ }
if (attributeId != AttributeId.Value) {
address += ";a=" + attributeId.name();
}
@@ -185,12 +266,13 @@ public class OpcuaTag implements PlcSubscriptionTag {
identifier.equals(that.identifier) &&
identifierType == that.identifierType &&
attributeId == that.attributeId &&
+ Objects.equals(indexRange, that.indexRange) &&
config.equals(that.config);
}
@Override
public int hashCode() {
- return Objects.hash(namespace, identifier, identifierType,
attributeId, config);
+ return Objects.hash(namespace, identifier, identifierType,
attributeId, indexRange, config);
}
@Override
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 c04cf5b818..6f75866b01 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
@@ -35,7 +35,10 @@ import org.apache.plc4x.java.DefaultPlcDriverManager;
import org.apache.plc4x.java.api.PlcConnection;
import org.apache.plc4x.java.api.PlcConnectionManager;
import org.apache.plc4x.java.api.PlcDriverManager;
+import org.apache.plc4x.java.api.authentication.PlcNullAuthentication;
import
org.apache.plc4x.java.api.authentication.PlcUsernamePasswordAuthentication;
+import org.apache.plc4x.java.spi.values.*;
+import org.apache.plc4x.java.utils.testutils.manual.BasicPlcTest;
import org.apache.plc4x.java.api.exceptions.PlcUnsupportedDataTypeException;
import org.apache.plc4x.java.api.messages.PlcReadRequest;
import org.apache.plc4x.java.api.messages.PlcReadResponse;
@@ -282,6 +285,177 @@ public class OpcuaPlcDriverTest {
}
}
+ @Test
+ void plc4xTestNamespaceExposesScalarsArraysAndMatrices() throws Exception {
+ // The custom Plc4xTestNamespace (ns=3;s=Test/...) mirrors the ADS
manual-test structure so
+ // the driver can be exercised against the same matrix of addressing
variants. This verifies
+ // the namespace is present and that scalars, whole arrays and
multi-dimensional matrices
+ // read back correctly.
+ try (PlcConnection connection = new
DefaultPlcDriverManager().getConnection(tcpConnectionAddress)) {
+ PlcReadResponse response = connection.readRequestBuilder()
+ .addTagAddress("bool", "ns=3;s=Test/Scalar/Bool")
+ .addTagAddress("dint", "ns=3;s=Test/Scalar/DInt")
+ .addTagAddress("string", "ns=3;s=Test/Scalar/String")
+ .addTagAddress("intArray", "ns=3;s=Test/Array/Int")
+ .addTagAddress("intMatrix", "ns=3;s=Test/Matrix/Int_2x3")
+ .build().execute().get(30, TimeUnit.SECONDS);
+
+ for (String tag : new String[]{"bool", "dint", "string",
"intArray", "intMatrix"}) {
+ assertThat(response.getResponseCode(tag)).describedAs("read of
'%s'", tag)
+ .isEqualTo(PlcResponseCode.OK);
+ }
+
+ // Scalars
+ assertThat(response.getBoolean("bool")).isTrue();
+ assertThat(response.getInteger("dint")).isEqualTo(-12345678);
+ assertThat(response.getString("string")).isEqualTo("Hello PLC4X");
+
+ // Whole 1-D array -> PlcList of 5 Int16 values.
+ org.apache.plc4x.java.api.value.PlcValue intArray =
response.getPlcValue("intArray");
+ assertThat(intArray.isList()).isTrue();
+ assertThat(intArray.getList()).hasSize(5);
+ assertThat(intArray.getList().get(0).getShort()).isEqualTo((short)
-3);
+ assertThat(intArray.getList().get(4).getShort()).isEqualTo((short)
3);
+
+ // Whole 2-D matrix Int16[2][3] -> nested PlcList (2 rows x 3
cols).
+ org.apache.plc4x.java.api.value.PlcValue matrix =
response.getPlcValue("intMatrix");
+ assertThat(matrix.isList()).isTrue();
+ assertThat(matrix.getList()).hasSize(2);
+
assertThat(matrix.getList().get(0).getList().get(0).getShort()).isEqualTo((short)
10);
+
assertThat(matrix.getList().get(1).getList().get(2).getShort()).isEqualTo((short)
-12);
+ }
+ }
+
+ @Test
+ void readsAndWritesArrayElementsViaIndexRange() throws Exception {
+ // Array/Int = {-3, -1, 0, 1, 3}; Matrix/Int_2x3 =
{{10,11,12},{-10,-11,-12}}.
+ try (PlcConnection connection = new
DefaultPlcDriverManager().getConnection(tcpConnectionAddress)) {
+ PlcReadResponse read = connection.readRequestBuilder()
+ .addTagAddress("elem", "ns=3;s=Test/Array/Int[3]") //
single element -> scalar
+ .addTagAddress("elemBase1", "ns=3;s=Test/Array/Int[4;1]") //
same element, 1-based
+ .addTagAddress("slice", "ns=3;s=Test/Array/Int[1..3]") //
inclusive range -> list
+ .addTagAddress("cell", "ns=3;s=Test/Matrix/Int_2x3[1][2]") //
matrix element -> scalar
+ .build().execute().get(30, TimeUnit.SECONDS);
+
+ for (String tag : new String[]{"elem", "elemBase1", "slice",
"cell"}) {
+ assertThat(read.getResponseCode(tag)).describedAs("read of
'%s'", tag)
+ .isEqualTo(PlcResponseCode.OK);
+ }
+ // [3] -> index 3 (0-based) -> value 1.
+ assertThat(read.getShort("elem")).isEqualTo((short) 1);
+ // [4;1] -> base 1 -> index 3 -> value 1 (same element).
+ assertThat(read.getShort("elemBase1")).isEqualTo((short) 1);
+ // [1..3] -> indices 1,2,3 -> {-1, 0, 1}.
+ assertThat(read.getPlcValue("slice").getList()).hasSize(3);
+
assertThat(read.getPlcValue("slice").getList().get(0).getShort()).isEqualTo((short)
-1);
+
assertThat(read.getPlcValue("slice").getList().get(2).getShort()).isEqualTo((short)
1);
+ // Matrix [1][2] -> row 1, col 2 -> -12.
+ assertThat(read.getShort("cell")).isEqualTo((short) -12);
+
+ // Indexed write: overwrite a single element of Array/DInt and
read it back via IndexRange.
+ connection.writeRequestBuilder()
+ .addTagAddress("w", "ns=3;s=Test/Array/DInt[2]", 424242)
+ .build().execute().get(30, TimeUnit.SECONDS);
+ PlcReadResponse back = connection.readRequestBuilder()
+ .addTagAddress("w", "ns=3;s=Test/Array/DInt[2]")
+ .build().execute().get(30, TimeUnit.SECONDS);
+
assertThat(back.getResponseCode("w")).isEqualTo(PlcResponseCode.OK);
+ assertThat(back.getInteger("w")).isEqualTo(424242);
+
+ // Restore the original value (0) so the shared server stays
pristine for other tests.
+ connection.writeRequestBuilder()
+ .addTagAddress("w", "ns=3;s=Test/Array/DInt[2]", 0)
+ .build().execute().get(30, TimeUnit.SECONDS);
+ }
+ }
+
+ @Test
+ void comprehensiveAddressingVariants() throws Exception {
+ // Mirrors the ADS manual test (ManualFactoryAdsDriverTestTC3) against
the Plc4xTestNamespace:
+ // the same matrix of addressing variants — scalars, whole arrays,
array elements/slices and
+ // multi-dimensional matrices — driven through the generic
BasicPlcTest harness.
+
+ // (A) Read + write coverage for the verified-writable set: scalars,
1-D arrays, 1-D index.
+ BasicPlcTest readWrite = new BasicPlcTest(tcpConnectionAddress, new
PlcNullAuthentication(),
+ true, true, true, true, 3);
+ // Scalars
+ readWrite.addTestCase("ns=3;s=Test/Scalar/Bool", new PlcBOOL(true));
+ readWrite.addTestCase("ns=3;s=Test/Scalar/SInt", new PlcSINT(-12));
+ readWrite.addTestCase("ns=3;s=Test/Scalar/USInt", new PlcUSINT(250));
+ readWrite.addTestCase("ns=3;s=Test/Scalar/Int", new PlcINT(-1234));
+ readWrite.addTestCase("ns=3;s=Test/Scalar/UInt", new PlcUINT(54321));
+ readWrite.addTestCase("ns=3;s=Test/Scalar/DInt", new
PlcDINT(-12345678));
+ readWrite.addTestCase("ns=3;s=Test/Scalar/UDInt", new
PlcUDINT(305419896L));
+ readWrite.addTestCase("ns=3;s=Test/Scalar/LInt", new
PlcLINT(-9223372036854770000L));
+ readWrite.addTestCase("ns=3;s=Test/Scalar/ULInt", new PlcULINT(new
BigInteger("18446744073709551000")));
+ readWrite.addTestCase("ns=3;s=Test/Scalar/Real", new
PlcREAL(3.14159f));
+ readWrite.addTestCase("ns=3;s=Test/Scalar/LReal", new
PlcLREAL(2.718281828459045d));
+ readWrite.addTestCase("ns=3;s=Test/Scalar/String", new
PlcSTRING("Hello PLC4X"));
+ // Whole 1-D arrays
+ readWrite.addTestCase("ns=3;s=Test/Array/Bool", new PlcList(List.of(
+ new PlcBOOL(true), new PlcBOOL(false), new PlcBOOL(true), new
PlcBOOL(true),
+ new PlcBOOL(false), new PlcBOOL(false), new PlcBOOL(true), new
PlcBOOL(false))));
+ readWrite.addTestCase("ns=3;s=Test/Array/Int", new PlcList(List.of(
+ new PlcINT(-3), new PlcINT(-1), new PlcINT(0), new PlcINT(1), new
PlcINT(3))));
+ readWrite.addTestCase("ns=3;s=Test/Array/UInt", new PlcList(List.of(
+ new PlcUINT(1), new PlcUINT(10), new PlcUINT(100), new
PlcUINT(1000), new PlcUINT(10000))));
+ readWrite.addTestCase("ns=3;s=Test/Array/DInt", new PlcList(List.of(
+ new PlcDINT(-1000), new PlcDINT(-500), new PlcDINT(0), new
PlcDINT(1000000), new PlcDINT(2000000))));
+ readWrite.addTestCase("ns=3;s=Test/Array/LReal", new PlcList(List.of(
+ new PlcLREAL(1.5), new PlcLREAL(-2.0), new PlcLREAL(0.125))));
+ readWrite.addTestCase("ns=3;s=Test/Array/String", new PlcList(List.of(
+ new PlcSTRING("alpha"), new PlcSTRING("beta"), new
PlcSTRING("gamma"))));
+ // Array element / slice via IndexRange (single element -> scalar,
range -> list)
+ readWrite.addTestCase("ns=3;s=Test/Array/Int[3]", new PlcINT(1));
+ readWrite.addTestCase("ns=3;s=Test/Array/Int[1..3]", new
PlcList(List.of(
+ new PlcINT(-1), new PlcINT(0), new PlcINT(1))));
+ readWrite.addTestCase("ns=3;s=Test/Array/DInt[0]", new PlcDINT(-1000));
+ readWrite.addTestCase("ns=3;s=Test/Array/DInt[4]", new
PlcDINT(2000000));
+ readWrite.run();
+
+ // (B) Read-only coverage for multi-dimensional matrices (whole +
element access). The read
+ // decode (nested PlcList / element selection) is the interesting part
here; multidimensional
+ // writes are exercised elsewhere.
+ BasicPlcTest readOnly = new BasicPlcTest(tcpConnectionAddress, new
PlcNullAuthentication(),
+ true, false, true, true, 2);
+ readOnly.addTestCase("ns=3;s=Test/Matrix/Int_2x3", new PlcList(List.of(
+ new PlcList(List.of(new PlcINT(10), new PlcINT(11), new
PlcINT(12))),
+ new PlcList(List.of(new PlcINT(-10), new PlcINT(-11), new
PlcINT(-12))))));
+ readOnly.addTestCase("ns=3;s=Test/Matrix/Real_3x2", new
PlcList(List.of(
+ new PlcList(List.of(new PlcREAL(1.0f), new PlcREAL(1.5f))),
+ new PlcList(List.of(new PlcREAL(2.0f), new PlcREAL(2.5f))),
+ new PlcList(List.of(new PlcREAL(3.0f), new PlcREAL(3.5f))))));
+ readOnly.addTestCase("ns=3;s=Test/Matrix/UInt_2x2x2", new
PlcList(List.of(
+ new PlcList(List.of(
+ new PlcList(List.of(new PlcUINT(1), new PlcUINT(2))),
+ new PlcList(List.of(new PlcUINT(3), new PlcUINT(4))))),
+ new PlcList(List.of(
+ new PlcList(List.of(new PlcUINT(5), new PlcUINT(6))),
+ new PlcList(List.of(new PlcUINT(7), new PlcUINT(8))))))));
+ // Matrix element via IndexRange -> scalar.
+ readOnly.addTestCase("ns=3;s=Test/Matrix/Int_2x3[0][0]", new
PlcINT(10));
+ readOnly.addTestCase("ns=3;s=Test/Matrix/Int_2x3[1][2]", new
PlcINT(-12));
+ readOnly.addTestCase("ns=3;s=Test/Matrix/UInt_2x2x2[1][1][1]", new
PlcUINT(8));
+ readOnly.run();
+ }
+
+ @org.junit.jupiter.api.Disabled("Enable once OPC UA struct (PlcStruct)
support and the custom "
+ + "struct nodes in Plc4xTestNamespace land (feature Phase 5 + task
T1b).")
+ @Test
+ void comprehensiveStructsSpec() throws Exception {
+ // Acceptance spec for struct addressing. OPC UA has no per-field node
addressing like ADS
+ // (g_simple.s8): a struct is one node whose value is a PlcStruct,
navigated client-side.
+ BasicPlcTest test = new BasicPlcTest(tcpConnectionAddress, new
PlcNullAuthentication(),
+ true, true, true, true, 1);
+ java.util.Map<String, org.apache.plc4x.java.api.value.PlcValue> simple
= new java.util.LinkedHashMap<>();
+ simple.put("s8", new PlcSINT(-8));
+ simple.put("u16", new PlcUINT(1600));
+ simple.put("r64", new PlcLREAL(-0.125d));
+ simple.put("str", new PlcSTRING("struct-string"));
+ test.addTestCase("ns=3;s=Test/Struct/Simple", new PlcStruct(simple));
+ test.run();
+ }
+
@Test
void resolvesAndCachesNodeTypes() throws Exception {
try (PlcConnection connection = new
DefaultPlcDriverManager().getConnection(tcpConnectionAddress)) {
diff --git
a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/tag/OpcuaTagIndexRangeTest.java
b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/tag/OpcuaTagIndexRangeTest.java
new file mode 100644
index 0000000000..c5479db201
--- /dev/null
+++
b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/tag/OpcuaTagIndexRangeTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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
+ *
+ * https://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.tag;
+
+import org.apache.plc4x.java.api.exceptions.PlcInvalidTagException;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests the array-index / IndexRange syntax on OPC UA tag addresses: {@code
[n]}, {@code [lo..hi]}
+ * (inclusive), an optional {@code ;base} lower bound (default 0), and the
multi-dimensional form
+ * {@code [..][..]}. The resolved value is an OPC UA IndexRange string
(0-based, inclusive, comma
+ * separated per dimension).
+ */
+class OpcuaTagIndexRangeTest {
+
+ @Test
+ void singleIndexIsZeroBased() {
+ OpcuaTag tag = OpcuaTag.of("ns=3;s=Test/Array/Int[8]");
+ // The index suffix is split off the identifier; the node address
itself is unchanged.
+ assertThat(tag.getIdentifier()).isEqualTo("Test/Array/Int");
+ assertThat(tag.getIndexRange()).isEqualTo("8");
+ // The index suffix survives in the address string (";NULL" is the
untyped-tag type tail),
+ // and re-parsing the address yields an equivalent tag.
+
assertThat(tag.getAddressString()).startsWith("ns=3;s=Test/Array/Int[8]");
+ OpcuaTag reparsed = OpcuaTag.of(tag.getAddressString());
+ assertThat(reparsed.getIdentifier()).isEqualTo("Test/Array/Int");
+ assertThat(reparsed.getIndexRange()).isEqualTo("8");
+ }
+
+ @Test
+ void inclusiveRange() {
+ // [3..8] -> indices 3..8 inclusive.
+
assertThat(OpcuaTag.of("ns=3;s=Foo[3..8]").getIndexRange()).isEqualTo("3:8");
+ }
+
+ @Test
+ void baseIsSubtracted() {
+ // [3..8;1] -> base 1, so the 3rd..8th element -> 0-based OPC UA range
2:7.
+
assertThat(OpcuaTag.of("ns=3;s=Foo[3..8;1]").getIndexRange()).isEqualTo("2:7");
+ // Single index with a base.
+
assertThat(OpcuaTag.of("ns=3;s=Foo[8;1]").getIndexRange()).isEqualTo("7");
+ }
+
+ @Test
+ void multiDimensional() {
+
assertThat(OpcuaTag.of("ns=3;s=Foo[1..2][0..1]").getIndexRange()).isEqualTo("1:2,0:1");
+
assertThat(OpcuaTag.of("ns=3;s=Foo[1][2][3]").getIndexRange()).isEqualTo("1,2,3");
+ // Per-dimension base.
+
assertThat(OpcuaTag.of("ns=3;s=Foo[1..6;1][3]").getIndexRange()).isEqualTo("0:5,3");
+ }
+
+ @Test
+ void noIndexLeavesTheTagUntouched() {
+ OpcuaTag tag = OpcuaTag.of("ns=3;s=Test/Scalar/Bool");
+ assertThat(tag.getIndexRange()).isNull();
+ assertThat(tag.getIdentifier()).isEqualTo("Test/Scalar/Bool");
+ }
+
+ @Test
+ void nonNumericBracketsAreNotAnIndex() {
+ // A string identifier that legitimately contains '[...]' with
non-numeric content is left
+ // whole (the index grammar is strictly numeric), so such nodes remain
addressable.
+ OpcuaTag tag = OpcuaTag.of("ns=3;s=Foo[bar]");
+ assertThat(tag.getIndexRange()).isNull();
+ assertThat(tag.getIdentifier()).isEqualTo("Foo[bar]");
+ }
+
+ @Test
+ void indexCombinesWithAttributeAndType() {
+ OpcuaTag tag = OpcuaTag.of("ns=3;s=Foo[3..8];INT");
+ assertThat(tag.getIdentifier()).isEqualTo("Foo");
+ assertThat(tag.getIndexRange()).isEqualTo("3:8");
+ assertThat(tag.getDataType().name()).isEqualTo("INT");
+ }
+
+ @Test
+ void invalidRangesAreRejected() {
+ // high < low
+ assertThatThrownBy(() -> OpcuaTag.of("ns=3;s=Foo[8..3]"))
+ .isInstanceOf(PlcInvalidTagException.class);
+ // base pushes an index negative
+ assertThatThrownBy(() -> OpcuaTag.of("ns=3;s=Foo[0..5;1]"))
+ .isInstanceOf(PlcInvalidTagException.class);
+ }
+}
diff --git
a/plc4j/drivers/opcua/src/test/java/org/eclipse/milo/examples/server/Plc4xTestNamespace.java
b/plc4j/drivers/opcua/src/test/java/org/eclipse/milo/examples/server/Plc4xTestNamespace.java
new file mode 100644
index 0000000000..abe1c054c0
--- /dev/null
+++
b/plc4j/drivers/opcua/src/test/java/org/eclipse/milo/examples/server/Plc4xTestNamespace.java
@@ -0,0 +1,241 @@
+/*
+ * 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.eclipse.milo.examples.server;
+
+import static
org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ubyte;
+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;
+import static
org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ushort;
+
+import java.util.List;
+import org.eclipse.milo.opcua.sdk.core.AccessLevel;
+import org.eclipse.milo.opcua.sdk.core.Reference;
+import org.eclipse.milo.opcua.sdk.core.ValueRank;
+import org.eclipse.milo.opcua.sdk.server.OpcUaServer;
+import org.eclipse.milo.opcua.sdk.server.api.DataItem;
+import org.eclipse.milo.opcua.sdk.server.api.ManagedNamespaceWithLifecycle;
+import org.eclipse.milo.opcua.sdk.server.api.MonitoredItem;
+import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel;
+import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode;
+import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode;
+import org.eclipse.milo.opcua.stack.core.Identifiers;
+import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
+import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
+import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
+import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
+import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
+import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort;
+
+/**
+ * A second server namespace (index 3) dedicated to PLC4X integration testing.
Where Milo's
+ * {@link ExampleNamespace} (ns=2, {@code HelloWorld/...}) exposes one node
per built-in type, this
+ * namespace mirrors the structure of the ADS manual test
+ * ({@code ManualFactoryAdsDriverTestTC3}) so the OPC UA driver can be
exercised against the same
+ * matrix of addressing variants: scalars, 1-D arrays and multi-dimensional
matrices/cubes (and,
+ * in a later increment, custom structs / nested structs / arrays of structs).
+ *
+ * <p>All values are fixed and distinctive so a client test can assert exact
round-trips. Node
+ * addresses are string identifiers of the form {@code
ns=3;s=Test/Scalar/Bool}.
+ */
+public class Plc4xTestNamespace extends ManagedNamespaceWithLifecycle {
+
+ public static final String NAMESPACE_URI = "urn:apache:plc4x:test";
+
+ private final SubscriptionModel subscriptionModel;
+
+ public Plc4xTestNamespace(OpcUaServer server) {
+ super(server, NAMESPACE_URI);
+
+ subscriptionModel = new SubscriptionModel(server, this);
+ getLifecycleManager().addLifecycle(subscriptionModel);
+ getLifecycleManager().addStartupTask(this::createAndAddNodes);
+ }
+
+ private void createAndAddNodes() {
+ UaFolderNode root = new UaFolderNode(
+ getNodeContext(),
+ newNodeId("Test"),
+ newQualifiedName("Test"),
+ LocalizedText.english("Test"));
+ getNodeManager().addNode(root);
+ // Surface the folder under the standard Objects folder so it is
browsable from the root.
+ root.addReference(new Reference(
+ root.getNodeId(),
+ Identifiers.Organizes,
+ Identifiers.ObjectsFolder.expanded(),
+ Reference.Direction.INVERSE));
+
+ addScalarNodes(root);
+ addArrayNodes(root);
+ addMatrixNodes(root);
+ }
+
+ //
=====================================================================================
+ // Scalars — one node per built-in type the driver maps to an IEC 61131-3
type.
+ //
=====================================================================================
+ private void addScalarNodes(UaFolderNode root) {
+ UaFolderNode folder = childFolder(root, "Scalar");
+ addVariable(folder, "Scalar/Bool", Identifiers.Boolean, new
Variant(true));
+ addVariable(folder, "Scalar/SInt", Identifiers.SByte, new
Variant((byte) -12));
+ addVariable(folder, "Scalar/USInt", Identifiers.Byte, new
Variant(ubyte(250)));
+ addVariable(folder, "Scalar/Int", Identifiers.Int16, new
Variant((short) -1234));
+ addVariable(folder, "Scalar/UInt", Identifiers.UInt16, new
Variant(ushort(54321)));
+ addVariable(folder, "Scalar/DInt", Identifiers.Int32, new
Variant(-12345678));
+ addVariable(folder, "Scalar/UDInt", Identifiers.UInt32, new
Variant(uint(305419896L)));
+ addVariable(folder, "Scalar/LInt", Identifiers.Int64, new
Variant(-9223372036854770000L));
+ addVariable(folder, "Scalar/ULInt", Identifiers.UInt64, new
Variant(ulong(new java.math.BigInteger("18446744073709551000"))));
+ addVariable(folder, "Scalar/Real", Identifiers.Float, new
Variant(3.14159f));
+ addVariable(folder, "Scalar/LReal", Identifiers.Double, new
Variant(2.718281828459045d));
+ addVariable(folder, "Scalar/String", Identifiers.String, new
Variant("Hello PLC4X"));
+ }
+
+ //
=====================================================================================
+ // 1-D arrays — whole-array reads (PlcList) and, via IndexRange,
element/slice reads.
+ //
=====================================================================================
+ private void addArrayNodes(UaFolderNode root) {
+ UaFolderNode folder = childFolder(root, "Array");
+ addArray(folder, "Array/Bool", Identifiers.Boolean,
ValueRank.OneDimension,
+ new Boolean[]{true, false, true, true, false, false, true, false});
+ addArray(folder, "Array/Int", Identifiers.Int16,
ValueRank.OneDimension,
+ new Short[]{-3, -1, 0, 1, 3});
+ addArray(folder, "Array/UInt", Identifiers.UInt16,
ValueRank.OneDimension,
+ new UShort[]{ushort(1), ushort(10), ushort(100), ushort(1000),
ushort(10000)});
+ addArray(folder, "Array/DInt", Identifiers.Int32,
ValueRank.OneDimension,
+ new Integer[]{-1000, -500, 0, 1000000, 2000000});
+ addArray(folder, "Array/LReal", Identifiers.Double,
ValueRank.OneDimension,
+ new Double[]{1.5d, -2.0d, 0.125d});
+ addArray(folder, "Array/String", Identifiers.String,
ValueRank.OneDimension,
+ new String[]{"alpha", "beta", "gamma"});
+ }
+
+ //
=====================================================================================
+ // Multi-dimensional arrays — matrix (2-D) and cube (3-D), whole-array +
IndexRange slices.
+ //
=====================================================================================
+ private void addMatrixNodes(UaFolderNode root) {
+ UaFolderNode folder = childFolder(root, "Matrix");
+
+ // INT[2][3]
+ short[][] matI16 = {{10, 11, 12}, {-10, -11, -12}};
+ addMatrix(folder, "Matrix/Int_2x3", Identifiers.Int16, matI16, new
UInteger[]{uint(2), uint(3)});
+
+ // REAL[3][2]
+ float[][] matR32 = {{1.0f, 1.5f}, {2.0f, 2.5f}, {3.0f, 3.5f}};
+ addMatrix(folder, "Matrix/Real_3x2", Identifiers.Float, matR32, new
UInteger[]{uint(3), uint(2)});
+
+ // UINT[2][2][2]
+ UShort[][][] cubeU16 = {
+ {{ushort(1), ushort(2)}, {ushort(3), ushort(4)}},
+ {{ushort(5), ushort(6)}, {ushort(7), ushort(8)}}
+ };
+ addMatrix(folder, "Matrix/UInt_2x2x2", Identifiers.UInt16, cubeU16,
+ new UInteger[]{uint(2), uint(2), uint(2)});
+ }
+
+ //
---------------------------------------------------------------------------------------
+ // Builders
+ //
---------------------------------------------------------------------------------------
+ private UaFolderNode childFolder(UaFolderNode parent, String name) {
+ UaFolderNode folder = new UaFolderNode(
+ getNodeContext(),
+ newNodeId("Test/" + name),
+ newQualifiedName(name),
+ LocalizedText.english(name));
+ getNodeManager().addNode(folder);
+ parent.addOrganizes(folder);
+ return folder;
+ }
+
+ private void addVariable(UaFolderNode folder, String id, NodeId dataType,
Variant value) {
+ UaVariableNode node = new
UaVariableNode.UaVariableNodeBuilder(getNodeContext())
+ .setNodeId(newNodeId("Test/" + id))
+ .setAccessLevel(AccessLevel.READ_WRITE)
+ .setUserAccessLevel(AccessLevel.READ_WRITE)
+ .setBrowseName(newQualifiedName(leaf(id)))
+ .setDisplayName(LocalizedText.english(leaf(id)))
+ .setDataType(dataType)
+ .setTypeDefinition(Identifiers.BaseDataVariableType)
+ .build();
+ node.setValue(new DataValue(value));
+ getNodeManager().addNode(node);
+ folder.addOrganizes(node);
+ }
+
+ private void addArray(UaFolderNode folder, String id, NodeId dataType,
ValueRank valueRank, Object array) {
+ UaVariableNode node = new
UaVariableNode.UaVariableNodeBuilder(getNodeContext())
+ .setNodeId(newNodeId("Test/" + id))
+ .setAccessLevel(AccessLevel.READ_WRITE)
+ .setUserAccessLevel(AccessLevel.READ_WRITE)
+ .setBrowseName(newQualifiedName(leaf(id)))
+ .setDisplayName(LocalizedText.english(leaf(id)))
+ .setDataType(dataType)
+ .setTypeDefinition(Identifiers.BaseDataVariableType)
+ .setValueRank(valueRank.getValue())
+ .setArrayDimensions(new UInteger[]{uint(0)})
+ .build();
+ node.setValue(new DataValue(new Variant(array)));
+ getNodeManager().addNode(node);
+ folder.addOrganizes(node);
+ }
+
+ private void addMatrix(UaFolderNode folder, String id, NodeId dataType,
Object array, UInteger[] dimensions) {
+ UaVariableNode node = new
UaVariableNode.UaVariableNodeBuilder(getNodeContext())
+ .setNodeId(newNodeId("Test/" + id))
+ .setAccessLevel(AccessLevel.READ_WRITE)
+ .setUserAccessLevel(AccessLevel.READ_WRITE)
+ .setBrowseName(newQualifiedName(leaf(id)))
+ .setDisplayName(LocalizedText.english(leaf(id)))
+ .setDataType(dataType)
+ .setTypeDefinition(Identifiers.BaseDataVariableType)
+ .setValueRank(dimensions.length)
+ .setArrayDimensions(dimensions)
+ .build();
+ node.setValue(new DataValue(new Variant(array)));
+ getNodeManager().addNode(node);
+ folder.addOrganizes(node);
+ }
+
+ private static String leaf(String id) {
+ int slash = id.lastIndexOf('/');
+ return slash < 0 ? id : id.substring(slash + 1);
+ }
+
+ //
---------------------------------------------------------------------------------------
+ // Subscription plumbing — delegate to the SubscriptionModel (required by
the base class).
+ //
---------------------------------------------------------------------------------------
+ @Override
+ public void onDataItemsCreated(List<DataItem> dataItems) {
+ subscriptionModel.onDataItemsCreated(dataItems);
+ }
+
+ @Override
+ public void onDataItemsModified(List<DataItem> dataItems) {
+ subscriptionModel.onDataItemsModified(dataItems);
+ }
+
+ @Override
+ public void onDataItemsDeleted(List<DataItem> dataItems) {
+ subscriptionModel.onDataItemsDeleted(dataItems);
+ }
+
+ @Override
+ public void onMonitoringModeChanged(List<MonitoredItem> monitoredItems) {
+ subscriptionModel.onMonitoringModeChanged(monitoredItems);
+ }
+}
diff --git
a/plc4j/drivers/opcua/src/test/java/org/eclipse/milo/examples/server/TestMiloServer.java
b/plc4j/drivers/opcua/src/test/java/org/eclipse/milo/examples/server/TestMiloServer.java
index c6e946cf48..840032701e 100644
---
a/plc4j/drivers/opcua/src/test/java/org/eclipse/milo/examples/server/TestMiloServer.java
+++
b/plc4j/drivers/opcua/src/test/java/org/eclipse/milo/examples/server/TestMiloServer.java
@@ -77,6 +77,7 @@ public class TestMiloServer {
private final Logger logger =
LoggerFactory.getLogger(TestMiloServer.class);
private final OpcUaServer server;
private final ExampleNamespace exampleNamespace;
+ private final Plc4xTestNamespace plc4xTestNamespace;
static {
// Required for SecurityPolicy.Aes256_Sha256_RsaPss
@@ -200,6 +201,11 @@ public class TestMiloServer {
getLifecycleManager().addStartupTask(new
EventNotifierTask(getServer()));
}};
exampleNamespace.startup();
+
+ // Second namespace (index 3) with the PLC4X addressing-variant test
nodes
+ // (ns=3;s=Test/...): scalars, arrays and multi-dimensional matrices.
+ plc4xTestNamespace = new Plc4xTestNamespace(server);
+ plc4xTestNamespace.startup();
}
private Set<EndpointConfiguration>
createEndpointConfigurations(X509Certificate certificate) {
@@ -315,6 +321,7 @@ public class TestMiloServer {
}
public CompletableFuture<OpcUaServer> shutdown() {
+ plc4xTestNamespace.shutdown();
exampleNamespace.shutdown();
return server.shutdown();