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
commit 5fbbea71a4f13caed56d5bba693cbcac35380754 Author: Christofer Dutz <[email protected]> AuthorDate: Tue Jul 14 14:25:20 2026 +0200 feat: Extended the opc-ua driver to derive the types for simple type tags from the server model (Instead of guessing it based on the input data) --- .../apache/plc4x/java/opcua/OpcuaConnection.java | 377 ++++++++++++++++----- .../plc4x/java/opcua/OpcuaPlcDriverTest.java | 104 +++++- .../manual/ManualOpcUaS71500NewFWDriverTest.java | 38 +-- 3 files changed, 395 insertions(+), 124 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 ec008bef76..3caabb747d 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 @@ -107,6 +107,12 @@ public class OpcuaConnection extends ConnectionBase<OpcuaConfiguration> implemen private Conversation conversation; private volatile boolean connected = false; + // Phase 3: per-session cache of server-resolved node attributes (data type, array shape, + // access rights), keyed by canonical node address ("ns=<ns>;<idType>=<id>"). OPC UA node + // types are static for the lifetime of a session, so once resolved they never need + // re-reading — both browsing and (Phase 4) typed read/write resolve through this cache. + private final Map<String, NodeAttributes> nodeTypeCache = new ConcurrentHashMap<>(); + public OpcuaConnection(OpcuaConfiguration configuration, TransportInstance<?> transportInstance, AuditLog auditLog) { @@ -571,26 +577,57 @@ public class OpcuaConnection extends ConnectionBase<OpcuaConfiguration> implemen private static final AttributeId[] BROWSE_ATTRIBUTES = { AttributeId.DataType, AttributeId.ValueRank, AttributeId.ArrayDimensions, AttributeId.AccessLevel}; + // Abstract numeric supertype DataType NodeIds (namespace 0). A variable typed with one of these + // accepts any concrete subtype, so there is no single built-in type to resolve to — the concrete + // type is chosen from the written value instead. Only UInteger needs special handling: plain + // (signed) value inference already yields a valid subtype for Integer/Number. + private static final long UINTEGER_DATATYPE_ID = 28L; + /** The server-resolved metadata of a single variable node. */ - private static final class NodeAttributes { + static final class NodeAttributes { private final OpcuaDataType dataType; // resolved built-in type, or null if unresolved + private final boolean unsigned; // node is the abstract UInteger supertype private final List<ArrayInfo> arrayInfo; // empty for scalars private final boolean readable; private final boolean writable; - private NodeAttributes(OpcuaDataType dataType, List<ArrayInfo> arrayInfo, boolean readable, boolean writable) { + private NodeAttributes(OpcuaDataType dataType, boolean unsigned, List<ArrayInfo> arrayInfo, + boolean readable, boolean writable) { this.dataType = dataType; + this.unsigned = unsigned; this.arrayInfo = arrayInfo; this.readable = readable; this.writable = writable; } + + OpcuaDataType dataType() { + return dataType; + } + + boolean unsigned() { + return unsigned; + } + + List<ArrayInfo> arrayInfo() { + return arrayInfo; + } + + boolean readable() { + return readable; + } + + boolean writable() { + return writable; + } } /** - * Reads DataType/ValueRank/ArrayDimensions/AccessLevel for every variable node among the given - * references in a single batched {@link ReadRequest} and returns a map keyed by node address. - * Object/method/other node classes are skipped (they have no such attributes). If the read - * fails the browse still succeeds — items just fall back to the NodeClass heuristic. + * Resolves DataType/ValueRank/ArrayDimensions/AccessLevel for every variable node among the + * given references and returns a map keyed by node address. Nodes already resolved this session + * are served from {@link #nodeTypeCache}; only the rest incur a (batched) server read. Object/ + * method/other node classes are skipped (they have no such attributes). If the read fails the + * browse still succeeds — items just fall back to the NodeClass heuristic (and any cached + * entries are still applied). */ private CompletableFuture<Map<String, NodeAttributes>> resolveVariableAttributes(List<ReferenceDescription> references) { List<String> variableAddresses = new ArrayList<>(); @@ -608,11 +645,57 @@ public class OpcuaConnection extends ConnectionBase<OpcuaConfiguration> implemen return CompletableFuture.completedFuture(Collections.emptyMap()); } - List<ReadValueId> readValueIds = new ArrayList<>(variableAddresses.size() * BROWSE_ATTRIBUTES.length); + // Serve already-resolved nodes straight from the session cache; only read the remainder. + Map<String, NodeAttributes> resolved = new HashMap<>(); + List<String> toRead = new ArrayList<>(); for (String address : variableAddresses) { - NodeId childNodeId = generateNodeId(OpcuaTag.of(address)); + NodeAttributes cached = nodeTypeCache.get(address); + if (cached != null) { + resolved.put(address, cached); + } else { + toRead.add(address); + } + } + if (toRead.isEmpty()) { + return CompletableFuture.completedFuture(resolved); + } + + return readNodeAttributes(toRead).thenApply(fresh -> { + resolved.putAll(fresh); + return resolved; + }).exceptionally(error -> { + LOGGER.warn("Failed to resolve variable node attributes during browse; " + + "browse items fall back to the NodeClass heuristic", error); + return resolved; + }); + } + + /** + * Lazily resolves the server-side attributes of a single node, caching the result for the + * session. This is the entry point for typed reads/writes: on a cache hit no server round-trip + * happens; on a miss the four attributes are read and cached. Returns {@code null} attributes + * only if the read yields nothing usable. + */ + CompletableFuture<NodeAttributes> resolveNodeAttributes(OpcuaTag tag) { + String key = cacheKey(tag); + NodeAttributes cached = nodeTypeCache.get(key); + if (cached != null) { + return CompletableFuture.completedFuture(cached); + } + return readNodeAttributes(Collections.singletonList(key)).thenApply(fresh -> fresh.get(key)); + } + + /** + * Issues one batched {@link ReadRequest} for the {@link #BROWSE_ATTRIBUTES} of every given node + * address, stores each resolved {@link NodeAttributes} in {@link #nodeTypeCache} and returns + * them keyed by address. + */ + private CompletableFuture<Map<String, NodeAttributes>> readNodeAttributes(List<String> addresses) { + List<ReadValueId> readValueIds = new ArrayList<>(addresses.size() * BROWSE_ATTRIBUTES.length); + for (String address : addresses) { + NodeId nodeId = generateNodeId(OpcuaTag.of(address)); for (AttributeId attributeId : BROWSE_ATTRIBUTES) { - readValueIds.add(new ReadValueId(childNodeId, attributeId.getValue(), + readValueIds.add(new ReadValueId(nodeId, attributeId.getValue(), NULL_STRING, new QualifiedName(0, NULL_STRING))); } } @@ -620,50 +703,72 @@ public class OpcuaConnection extends ConnectionBase<OpcuaConfiguration> implemen TimestampsToReturn.timestampsToReturnNeither, readValueIds); return conversation.submit(readRequest, ReadResponse.class).thenApply(response -> { - Map<String, NodeAttributes> resolved = new HashMap<>(); List<DataValue> results = response.getResults(); - int index = 0; - for (String address : variableAddresses) { - DataValue dataTypeValue = results.get(index++); - DataValue valueRankValue = results.get(index++); - DataValue arrayDimensionsValue = results.get(index++); - DataValue accessLevelValue = results.get(index++); - - Short accessLevel = byteValue(accessLevelValue); - // AccessLevel bit0 = CurrentRead, bit1 = CurrentWrite. If the attribute couldn't be - // read, assume read/write (matching the earlier NodeClass-based default). - boolean readable = accessLevel == null || (accessLevel & 0x01) != 0; - boolean writable = accessLevel == null || (accessLevel & 0x02) != 0; - - resolved.put(address, new NodeAttributes( - resolveDataType(dataTypeValue), - arrayInfoOf(valueRankValue, arrayDimensionsValue), - readable, writable)); + Map<String, NodeAttributes> fresh = new LinkedHashMap<>(); + for (int i = 0; i < addresses.size(); i++) { + String address = addresses.get(i); + NodeAttributes attributes = attributesFrom(results, i * BROWSE_ATTRIBUTES.length); + nodeTypeCache.put(address, attributes); + fresh.put(address, attributes); } - return resolved; - }).exceptionally(error -> { - LOGGER.warn("Failed to resolve variable node attributes during browse; " - + "browse items fall back to the NodeClass heuristic", error); - return Collections.emptyMap(); + return fresh; }); } /** - * Resolves the DataType attribute (a NodeId) to an {@link OpcuaDataType}. Built-in DataType - * NodeIds live in namespace 0 and share their numeric identifier with the OPC UA Variant - * built-in type (Boolean=1, Int32=6, ...), so we can map straight through. Custom/enum/struct - * data types (non-namespace-0 or non-numeric) are left unresolved for a later phase. + * Builds a {@link NodeAttributes} from the four consecutive {@link DataValue}s (DataType, + * ValueRank, ArrayDimensions, AccessLevel — in {@link #BROWSE_ATTRIBUTES} order) starting at + * {@code offset} in a Read response. */ - private static OpcuaDataType resolveDataType(DataValue dataValue) { + private static NodeAttributes attributesFrom(List<DataValue> results, int offset) { + DataValue dataTypeValue = results.get(offset); + DataValue valueRankValue = results.get(offset + 1); + DataValue arrayDimensionsValue = results.get(offset + 2); + DataValue accessLevelValue = results.get(offset + 3); + + Short accessLevel = byteValue(accessLevelValue); + // AccessLevel bit0 = CurrentRead, bit1 = CurrentWrite. If the attribute couldn't be read, + // assume read/write (matching the earlier NodeClass-based default). + boolean readable = accessLevel == null || (accessLevel & 0x01) != 0; + boolean writable = accessLevel == null || (accessLevel & 0x02) != 0; + + NodeId dataTypeNodeId = dataTypeNodeIdOf(dataTypeValue); + return new NodeAttributes( + concreteDataType(dataTypeNodeId), + isUnsignedAbstract(dataTypeNodeId), + arrayInfoOf(valueRankValue, arrayDimensionsValue), + readable, writable); + } + + /** Canonical cache key for a tag: its node address without the attribute or data-type suffix. */ + private static String cacheKey(OpcuaTag tag) { + return String.format("ns=%d;%s=%s", tag.getNamespace(), + tag.getIdentifierType().getValue(), tag.getIdentifier()); + } + + /** Number of nodes whose attributes are currently cached for this session (test/diagnostic hook). */ + int nodeTypeCacheSize() { + return nodeTypeCache.size(); + } + + /** Extracts the DataType attribute's NodeId from its {@link DataValue}, or null if not present. */ + private static NodeId dataTypeNodeIdOf(DataValue dataValue) { if (dataValue == null || !dataValue.getValueSpecified() || !(dataValue.getValue() instanceof VariantNodeId)) { return null; } List<NodeId> nodeIds = ((VariantNodeId) dataValue.getValue()).getValue(); - if (nodeIds == null || nodeIds.isEmpty()) { - return null; - } - NodeId dataTypeNodeId = nodeIds.get(0); - if (namespaceOf(dataTypeNodeId) != 0) { + return (nodeIds == null || nodeIds.isEmpty()) ? null : nodeIds.get(0); + } + + /** + * Resolves a DataType NodeId to a concrete {@link OpcuaDataType}. Built-in DataType NodeIds live + * in namespace 0 and share their numeric identifier with the OPC UA Variant built-in type + * (Boolean=1, Int32=6, ...), so we can map straight through. Abstract supertypes and + * custom/enum/struct data types (non-namespace-0 or non-numeric) have no concrete built-in type + * and return null. + */ + private static OpcuaDataType concreteDataType(NodeId dataTypeNodeId) { + if (dataTypeNodeId == null || namespaceOf(dataTypeNodeId) != 0) { return null; } Long numericId = numericIdentifierOf(dataTypeNodeId); @@ -673,6 +778,15 @@ public class OpcuaConnection extends ConnectionBase<OpcuaConfiguration> implemen return OpcuaDataType.firstEnumForFieldVariantType(numericId.shortValue()); } + /** Whether a DataType NodeId is the abstract UInteger supertype (so writes must pick an unsigned type). */ + private static boolean isUnsignedAbstract(NodeId dataTypeNodeId) { + if (dataTypeNodeId == null || namespaceOf(dataTypeNodeId) != 0) { + return false; + } + Long numericId = numericIdentifierOf(dataTypeNodeId); + return numericId != null && numericId == UINTEGER_DATATYPE_ID; + } + /** * Builds the array shape from the ValueRank and ArrayDimensions attributes. ValueRank < 0 * (-1 scalar, -2 any, -3 scalar-or-one-dim) yields no array info; ValueRank >= 0 marks an @@ -1123,7 +1237,58 @@ public class OpcuaConnection extends ConnectionBase<OpcuaConfiguration> implemen return new PlcList(flat); } - private Variant fromPlcValue(String tagName, OpcuaTag tag, PlcWriteRequest request) { + /** + * Last-resort write type when neither a ;TYPE suffix nor a server-resolved type is available: + * infer from the PlcValue itself. Prefer the value's own declared PlcValueType; only when that + * is absent fall back to the backing Java class — which collapses the Short-backed types + * (BYTE/USINT/WORD/UINT) into INT, so the server may reject the write with INVALID_DATATYPE. + */ + private static PlcValueType inferWriteType(PlcValue value) { + PlcValueType inferred = value.getPlcValueType(); + if (inferred != null && inferred != PlcValueType.NULL) { + return inferred; + } + Object object = value.getObject(); + if (object instanceof Boolean) { + return PlcValueType.BOOL; + } else if (object instanceof Byte) { + return PlcValueType.SINT; + } else if (object instanceof Short) { + return PlcValueType.INT; + } else if (object instanceof Integer) { + return PlcValueType.DINT; + } else if (object instanceof Long) { + return PlcValueType.LINT; + } else if (object instanceof Float) { + return PlcValueType.REAL; + } else if (object instanceof Double) { + return PlcValueType.LREAL; + } else if (object instanceof String) { + return PlcValueType.STRING; + } + return PlcValueType.NULL; + } + + /** + * Write type for a node whose server DataType is the abstract UInteger supertype: pick the + * unsigned concrete type matching the value's width (any unsigned subtype is accepted). Falls + * back to plain inference for non-integer values. + */ + private static PlcValueType inferUnsignedWriteType(PlcValue value) { + Object object = value.getObject(); + if (object instanceof Byte) { + return PlcValueType.USINT; + } else if (object instanceof Short) { + return PlcValueType.UINT; + } else if (object instanceof Integer) { + return PlcValueType.UDINT; + } else if (object instanceof Long || object instanceof BigInteger) { + return PlcValueType.ULINT; + } + return inferWriteType(value); + } + + private Variant fromPlcValue(String tagName, OpcuaTag tag, PlcWriteRequest request, NodeAttributes serverAttributes) { PlcList valueObject; if (request.getPlcValue(tagName).getObject() instanceof List) { valueObject = (PlcList) request.getPlcValue(tagName); @@ -1146,29 +1311,19 @@ public class OpcuaConnection extends ConnectionBase<OpcuaConfiguration> implemen List<PlcValue> plcValueList = valueObject.getList(); PlcValueType dataType = tag.getPlcValueType(); if (dataType.equals(PlcValueType.NULL) || dataType.equals(PlcValueType.List)) { - // When the tag address didn't carry a type suffix, infer from the - // PlcValue's own type. Falling back on getObject().getClass() - // collapses BYTE/USINT/WORD/UINT (all back-by Short) into INT, - // so the server rejects the write with INVALID_DATATYPE. - PlcValueType inferred = plcValueList.get(0).getPlcValueType(); - if (inferred != null && inferred != PlcValueType.NULL) { - dataType = inferred; - } else if (plcValueList.get(0).getObject() instanceof Boolean) { - dataType = PlcValueType.BOOL; - } else if (plcValueList.get(0).getObject() instanceof Byte) { - dataType = PlcValueType.SINT; - } else if (plcValueList.get(0).getObject() instanceof Short) { - dataType = PlcValueType.INT; - } else if (plcValueList.get(0).getObject() instanceof Integer) { - dataType = PlcValueType.DINT; - } else if (plcValueList.get(0).getObject() instanceof Long) { - dataType = PlcValueType.LINT; - } else if (plcValueList.get(0).getObject() instanceof Float) { - dataType = PlcValueType.REAL; - } else if (plcValueList.get(0).getObject() instanceof Double) { - dataType = PlcValueType.LREAL; - } else if (plcValueList.get(0).getObject() instanceof String) { - dataType = PlcValueType.STRING; + // The tag address didn't carry a ;TYPE suffix. Phase 4: prefer the type the server + // declares for this node (resolved and cached up-front in onWrite) — it is + // authoritative and, unlike the Java-value guess below, unambiguously distinguishes + // the Short-backed types (BYTE/USINT/WORD/UINT/INT). + OpcuaDataType serverType = serverAttributes != null ? serverAttributes.dataType() : null; + if (serverType != null && serverType != OpcuaDataType.NULL) { + dataType = PlcValueType.valueOf(serverType.name()); + } else if (serverAttributes != null && serverAttributes.unsigned()) { + // Node is the abstract UInteger type: no concrete built-in to resolve to, so pick + // an unsigned type sized to the value (an abstract node accepts any subtype). + dataType = inferUnsignedWriteType(plcValueList.get(0)); + } else { + dataType = inferWriteType(plcValueList.get(0)); } } int length = valueObject.getLength(); @@ -1422,35 +1577,71 @@ public class OpcuaConnection extends ConnectionBase<OpcuaConfiguration> implemen LOGGER.trace("Writing Value"); DefaultPlcWriteRequest request = (DefaultPlcWriteRequest) writeRequest; - RequestHeader requestHeader = conversation.createRequestHeader(); - List<WriteValue> writeValueList = new ArrayList<>(request.getTagNames().size()); - for (String tagName : request.getTagNames()) { - OpcuaTag tag = (OpcuaTag) request.getTag(tagName); + // Phase 4: for tags without an explicit ;TYPE suffix, resolve the server-declared data + // type (via the session type cache) up-front so the write is built with the authoritative + // OPC UA type instead of a lossy Java-value guess. Then assemble and submit the write. + return resolveWriteTypes(request).thenCompose(serverAttributes -> { + RequestHeader requestHeader = conversation.createRequestHeader(); + List<WriteValue> writeValueList = new ArrayList<>(request.getTagNames().size()); + for (String tagName : request.getTagNames()) { + OpcuaTag tag = (OpcuaTag) request.getTag(tagName); + + NodeId nodeId = generateNodeId(tag); + + writeValueList.add(new WriteValue(nodeId, + tag.getAttributeId().getValue(), + NULL_STRING, + new DataValue( + false, + false, + false, + false, + false, + true, + fromPlcValue(tagName, tag, writeRequest, serverAttributes.get(tagName)), + null, + null, + null, + null, + null))); + } - NodeId nodeId = generateNodeId(tag); + WriteRequest opcuaWriteRequest = new WriteRequest(requestHeader, writeValueList); - writeValueList.add(new WriteValue(nodeId, - tag.getAttributeId().getValue(), - NULL_STRING, - new DataValue( - false, - false, - false, - false, - false, - true, - fromPlcValue(tagName, tag, writeRequest), - null, - null, - null, - null, - null))); - } - - WriteRequest opcuaWriteRequest = new WriteRequest(requestHeader, writeValueList); - - return conversation.submit(opcuaWriteRequest, WriteResponse.class) - .thenApply(response -> writeResponse(request, response)); + return conversation.submit(opcuaWriteRequest, WriteResponse.class) + .thenApply(response -> writeResponse(request, response)); + }); + } + + /** + * Resolves the server-declared attributes for every write tag that lacks an explicit ;TYPE + * suffix, returning a map (tagName -> {@link NodeAttributes}). Suffix-typed tags are skipped + * (the suffix wins) and failed lookups are simply omitted, so {@link #fromPlcValue} falls back + * to Java-value inference for them. + */ + private CompletableFuture<Map<String, NodeAttributes>> resolveWriteTypes(DefaultPlcWriteRequest request) { + Map<String, NodeAttributes> serverAttributes = new ConcurrentHashMap<>(); + List<CompletableFuture<?>> futures = new ArrayList<>(); + for (String tagName : request.getTagNames()) { + OpcuaTag tag = (OpcuaTag) request.getTag(tagName); + // An explicit ;TYPE suffix is authoritative — no server round-trip needed. + if (tag.getDataType() != OpcuaDataType.NULL) { + continue; + } + futures.add(resolveNodeAttributes(tag) + .thenAccept(attributes -> { + if (attributes != null) { + serverAttributes.put(tagName, attributes); + } + }) + .exceptionally(error -> { + LOGGER.debug("Could not resolve server type for write tag '{}'; " + + "falling back to value inference", tagName, error); + return null; + })); + } + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> serverAttributes); } private PlcWriteResponse writeResponse(DefaultPlcWriteRequest request, WriteResponse writeResponse) { 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 fc6daa2d51..c04cf5b818 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 @@ -282,6 +282,86 @@ public class OpcuaPlcDriverTest { } } + @Test + void resolvesAndCachesNodeTypes() throws Exception { + try (PlcConnection connection = new DefaultPlcDriverManager().getConnection(tcpConnectionAddress)) { + // getConnection hands back the driver connection directly, so we can reach the + // per-session type cache to observe Phase-3 behaviour. + OpcuaConnection opcua = (OpcuaConnection) connection; + assertThat(opcua.nodeTypeCacheSize()).isZero(); + + // A lazy single-node lookup misses the cache, reads the server, and caches the result. + OpcuaConnection.NodeAttributes boolAttrs = opcua + .resolveNodeAttributes(OpcuaTag.of("ns=2;s=HelloWorld/ScalarTypes/Boolean")) + .get(30, TimeUnit.SECONDS); + assertThat(boolAttrs.dataType()) + .isEqualTo(org.apache.plc4x.java.opcua.readwrite.OpcuaDataType.BOOL); + assertThat(boolAttrs.readable()).isTrue(); + assertThat(opcua.nodeTypeCacheSize()).isEqualTo(1); + + // Looking the same node up again is served from the cache — no new entry, no read. + opcua.resolveNodeAttributes(OpcuaTag.of("ns=2;s=HelloWorld/ScalarTypes/Boolean")) + .get(30, TimeUnit.SECONDS); + assertThat(opcua.nodeTypeCacheSize()).isEqualTo(1); + + // A different node is resolved (Int32 -> DINT) and added to the cache. + OpcuaConnection.NodeAttributes intAttrs = opcua + .resolveNodeAttributes(OpcuaTag.of("ns=2;s=HelloWorld/ScalarTypes/Int32")) + .get(30, TimeUnit.SECONDS); + assertThat(intAttrs.dataType()) + .isEqualTo(org.apache.plc4x.java.opcua.readwrite.OpcuaDataType.DINT); + assertThat(opcua.nodeTypeCacheSize()).isEqualTo(2); + + // Browsing shares the same cache: the two already-resolved children are reused and the + // remaining ScalarTypes children get resolved and cached, growing the cache further. + connection.browseRequestBuilder().addQuery("scalars", "ns=2;s=HelloWorld/ScalarTypes") + .build().execute().get(30, TimeUnit.SECONDS); + assertThat(opcua.nodeTypeCacheSize()).isGreaterThan(2); + } + } + + @Test + void writeWithoutTypeSuffixUsesServerType() throws Exception { + try (PlcConnection connection = new DefaultPlcDriverManager().getConnection(tcpConnectionAddress)) { + // None of these addresses carry a ";TYPE" suffix. Phase 4 resolves each node's real + // OPC UA type from the server, so the writes use the correct Variant type instead of a + // lossy Java-value guess. UInt16 is the telling case: 65535 arrives as a Java Integer, + // which value-inference would map to Int32 and the server would reject — the resolved + // UInt16 type makes it succeed. + PlcWriteResponse write = connection.writeRequestBuilder() + .addTagAddress("bool", BOOL_IDENTIFIER_READ_WRITE, true) + .addTagAddress("int16", INT16_IDENTIFIER_READ_WRITE, (short) -12345) + .addTagAddress("uint16", UINT16_IDENTIFIER_READ_WRITE, 65535) + .addTagAddress("int32", INT32_IDENTIFIER_READ_WRITE, 1234567) + .addTagAddress("float", FLOAT_IDENTIFIER_READ_WRITE, 3.5f) + .addTagAddress("string", STRING_IDENTIFIER_READ_WRITE, "phase4") + .build().execute().get(30, TimeUnit.SECONDS); + + for (String tag : new String[]{"bool", "int16", "uint16", "int32", "float", "string"}) { + assertThat(write.getResponseCode(tag)) + .describedAs("write of suffix-less tag '%s'", tag) + .isEqualTo(PlcResponseCode.OK); + } + + // Read the values back (also suffix-less) and confirm they round-tripped correctly. + PlcReadResponse read = connection.readRequestBuilder() + .addTagAddress("bool", BOOL_IDENTIFIER_READ_WRITE) + .addTagAddress("int16", INT16_IDENTIFIER_READ_WRITE) + .addTagAddress("uint16", UINT16_IDENTIFIER_READ_WRITE) + .addTagAddress("int32", INT32_IDENTIFIER_READ_WRITE) + .addTagAddress("float", FLOAT_IDENTIFIER_READ_WRITE) + .addTagAddress("string", STRING_IDENTIFIER_READ_WRITE) + .build().execute().get(30, TimeUnit.SECONDS); + + assertThat(read.getBoolean("bool")).isTrue(); + assertThat(read.getShort("int16")).isEqualTo((short) -12345); + assertThat(read.getInteger("uint16")).isEqualTo(65535); + assertThat(read.getInteger("int32")).isEqualTo(1234567); + assertThat(read.getFloat("float")).isEqualTo(3.5f); + assertThat(read.getString("string")).isEqualTo("phase4"); + } + } + private static void collectBrowseItems(java.util.List<org.apache.plc4x.java.api.messages.PlcBrowseItem> items, java.util.List<org.apache.plc4x.java.api.messages.PlcBrowseItem> out) { for (org.apache.plc4x.java.api.messages.PlcBrowseItem item : items) { @@ -521,24 +601,28 @@ public class OpcuaPlcDriverTest { @Nested class readWrite { + // Addresses no longer need a ";TYPE" suffix: the driver resolves each node's data type from + // the server (Phase 4) so writes use the correct OPC UA type without a hint — including the + // 'UInteger' node, whose abstract UInteger type is resolved to an unsigned type sized to the + // written value. Map<String, Entry<String, Object>> tags = Map.ofEntries( entry("Bool", entry(BOOL_IDENTIFIER_READ_WRITE, true)), - entry("Byte", entry(BYTE_IDENTIFIER_READ_WRITE + ";BYTE", (short) 3)), + entry("Byte", entry(BYTE_IDENTIFIER_READ_WRITE, (short) 3)), entry("Double", entry(DOUBLE_IDENTIFIER_READ_WRITE, 0.5d)), entry("Float", entry(FLOAT_IDENTIFIER_READ_WRITE, 0.5f)), - entry("Int16", entry(INT16_IDENTIFIER_READ_WRITE + ";INT", 1)), + entry("Int16", entry(INT16_IDENTIFIER_READ_WRITE, 1)), entry("Int32", entry(INT32_IDENTIFIER_READ_WRITE, 42)), entry("Int64", entry(INT64_IDENTIFIER_READ_WRITE, 42L)), entry("Integer", entry(INTEGER_IDENTIFIER_READ_WRITE, -127)), //entry("SByte", entry(SBYTE_IDENTIFIER_READ_WRITE, )), entry("String", entry(STRING_IDENTIFIER_READ_WRITE, "Hello Toddy!")), - entry("UInt16", entry(UINT16_IDENTIFIER_READ_WRITE + ";UINT", 65535)), - entry("UInt32", entry(UINT32_IDENTIFIER_READ_WRITE + ";UDINT", 101010101L)), - entry("UInt64", entry(UINT64_IDENTIFIER_READ_WRITE + ";ULINT", new BigInteger("1337"))), - entry("UInteger", entry(UINTEGER_IDENTIFIER_READ_WRITE + ";UDINT", 102020202L)), + entry("UInt16", entry(UINT16_IDENTIFIER_READ_WRITE, 65535)), + entry("UInt32", entry(UINT32_IDENTIFIER_READ_WRITE, 101010101L)), + entry("UInt64", entry(UINT64_IDENTIFIER_READ_WRITE, new BigInteger("1337"))), + entry("UInteger", entry(UINTEGER_IDENTIFIER_READ_WRITE, 102020202L)), entry("BooleanArray", entry(BOOL_ARRAY_IDENTIFIER, new boolean[]{true, true, true, true, true})), // entry("ByteStringArray", entry(BYTE_STRING_ARRAY_IDENTIFIER, null)), - entry("ByteArray", entry(BYTE_ARRAY_IDENTIFIER + ";BYTE", new Short[]{1, 100, 100, 255, 123})), + entry("ByteArray", entry(BYTE_ARRAY_IDENTIFIER, new Short[]{1, 100, 100, 255, 123})), entry("DoubleArray", entry(DOUBLE_ARRAY_IDENTIFIER, new Double[]{1.0, 2.0, 3.0, 4.0, 5.0})), entry("FloatArray", entry(FLOAT_ARRAY_IDENTIFIER, new Float[]{1.0F, 2.0F, 3.0F, 4.0F, 5.0F})), entry("Int16Array", entry(INT16_ARRAY_IDENTIFIER, new Short[]{1, 2, 3, 4, 5})), @@ -547,9 +631,9 @@ public class OpcuaPlcDriverTest { entry("IntegerArray", entry(INT32_ARRAY_IDENTIFIER, new Integer[]{1, 2, 3, 4, 5})), entry("SByteArray", entry(SBYTE_ARRAY_IDENTIFIER, new Byte[]{1, 2, 3, 4, 5})), entry("StringArray", entry(STRING_ARRAY_IDENTIFIER, new String[]{"1", "2", "3", "4", "5"})), - entry("UInt16Array", entry(UINT16_ARRAY_IDENTIFIER + ";UINT", new Short[]{1, 2, 3, 4, 5})), - entry("UInt32Array", entry(UINT32_ARRAY_IDENTIFIER + ";UDINT", new Integer[]{1, 2, 3, 4, 5})), - entry("UInt64Array", entry(UINT64_ARRAY_IDENTIFIER + ";ULINT", new Long[]{1L, 2L, 3L, 4L, 5L})), + entry("UInt16Array", entry(UINT16_ARRAY_IDENTIFIER, new Short[]{1, 2, 3, 4, 5})), + entry("UInt32Array", entry(UINT32_ARRAY_IDENTIFIER, new Integer[]{1, 2, 3, 4, 5})), + entry("UInt64Array", entry(UINT64_ARRAY_IDENTIFIER, new Long[]{1L, 2L, 3L, 4L, 5L})), entry(DOES_NOT_EXISTS_TAG_NAME, entry(DOES_NOT_EXIST_IDENTIFIER_READ_WRITE, "11")) ); diff --git a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/manual/ManualOpcUaS71500NewFWDriverTest.java b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/manual/ManualOpcUaS71500NewFWDriverTest.java index b1888bf732..1f8b23d925 100644 --- a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/manual/ManualOpcUaS71500NewFWDriverTest.java +++ b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/manual/ManualOpcUaS71500NewFWDriverTest.java @@ -22,6 +22,7 @@ import org.apache.plc4x.java.api.authentication.PlcNullAuthentication; import org.apache.plc4x.java.spi.values.*; import org.apache.plc4x.java.utils.testutils.manual.BasicPlcTest; +import java.math.BigDecimal; import java.time.Duration; import java.util.List; @@ -34,27 +35,22 @@ public class ManualOpcUaS71500NewFWDriverTest extends BasicPlcTest { public static void main(String[] args) throws Exception { boolean testArrays = true; ManualOpcUaS71500NewFWDriverTest test = new ManualOpcUaS71500NewFWDriverTest("opcua://192.168.24.66:4840"); - test.addTestCase(/*"g_b1",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_b1\";BOOL", new PlcBOOL(true)); - test.addTestCase(/*"g_b8",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_b8\";BYTE", new PlcBYTE(0xAB)); - test.addTestCase(/*"g_s8",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_s8\";SINT", new PlcSINT(-12)); - test.addTestCase(/*"g_u8",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_u8\";USINT", new PlcUSINT(250)); - // TODO: Getting "INVALID_ADDRESS", as WORD is not part of OpcuaDataType -// test.addTestCase(/*"g_b16",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_b16\";WORD", new PlcWORD(0xBEEF)); - test.addTestCase(/*"g_s16",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_s16\";INT", new PlcINT(-1234)); - test.addTestCase(/*"g_u16",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_u16\";UINT", new PlcUINT(54321)); - // TODO: Getting "INVALID_ADDRESS", as DWORD is not part of OpcuaDataType -// test.addTestCase(/*"g_b32",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_b32\";DWORD", new PlcDWORD(0xDEADBEEFL)); - test.addTestCase(/*"g_s32",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_s32\";DINT", new PlcDINT(-12345678)); - test.addTestCase(/*"g_u32",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_u32\";UDINT", new PlcUDINT(305419896)); - // TODO: Getting "INVALID_ADDRESS", as LWORD is not part of OpcuaDataType -// test.addTestCase(/*"g_b64",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_b64\";LWORD", new PlcLWORD(0x0123_4567_89AB_CDEFL)); - test.addTestCase(/*"g_s64",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_s64\";LINT", new PlcLINT(-9223372036854770000L)); - // TODO: This seems to write different values to the plc. -// test.addTestCase(/*"g_u64",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_u64\";ULINT", new PlcULINT(new BigDecimal("18446744073709551000"))); - test.addTestCase(/*"g_r32",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_r32\";REAL", new PlcREAL(3.14159)); - test.addTestCase(/*"g_r64",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_r64\";LREAL", new PlcLREAL(2.71828182845905)); - // TODO: Writing of TIME doesn't seem to be supported. -// test.addTestCase(/*"g_tim",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_tim\";TIME", new PlcTIME(2500)); // Is returned as Int32 + test.addTestCase(/*"g_b1",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_b1\"", new PlcBOOL(true)); + test.addTestCase(/*"g_b8",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_b8\"", new PlcBYTE(0xAB)); + test.addTestCase(/*"g_s8",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_s8\"", new PlcSINT(-12)); + test.addTestCase(/*"g_u8",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_u8\"", new PlcUSINT(250)); + test.addTestCase(/*"g_b16",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_b16\"", new PlcWORD(0xBEEF)); + test.addTestCase(/*"g_s16",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_s16\"", new PlcINT(-1234)); + test.addTestCase(/*"g_u16",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_u16\"", new PlcUINT(54321)); + test.addTestCase(/*"g_b32",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_b32\"", new PlcDWORD(0xDEADBEEFL)); + test.addTestCase(/*"g_s32",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_s32\"", new PlcDINT(-12345678)); + test.addTestCase(/*"g_u32",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_u32\"", new PlcUDINT(305419896)); + test.addTestCase(/*"g_b64",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_b64\"", new PlcLWORD(0x0123_4567_89AB_CDEFL)); + test.addTestCase(/*"g_s64",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_s64\"", new PlcLINT(-9223372036854770000L)); + test.addTestCase(/*"g_u64",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_u64\"", new PlcULINT(new BigDecimal("18446744073709551000"))); + test.addTestCase(/*"g_r32",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_r32\"", new PlcREAL(3.14159)); + test.addTestCase(/*"g_r64",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_r64\"", new PlcLREAL(2.71828182845905)); + test.addTestCase(/*"g_tim",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_tim\"", new PlcTIME(2500)); // Is returned as Int32 // TODO: IEC 61131-3 date types use 1990-01-01 as epoch, PlcDATE etc. use Unix epoch (1970-01-01). // test.addTestCase(/*"g_dat",*/ "ns=3;s=\"OPC_UA_DB\".\"OPC Data\".\"g_dat\";DATE", new PlcDATE(LocalDate.of(2025, 11, 12))); // Is returned as UInt16 // TODO: S7/IEC value is milliseconds since midnight
