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]} -&gt; {@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();

Reply via email to