You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@plc4x.apache.org by cd...@apache.org on 2026/07/14 12:25:36 UTC
(plc4x) branch develop updated (955486e0fb -> 015924e699)
This is an automated email from the ASF dual-hosted git repository.
chrisdutz pushed a change to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git
from 955486e0fb fix: Make the lease-time decision deterministic (purely idle-time based, plus the force flag)
new 5fbbea71a4 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)
new 015924e699 chore: Updated the KNX Vendor IDs
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../java/knxnetip/readwrite/KnxManufacturer.java | 6 +-
.../apache/plc4x/java/opcua/OpcuaConnection.java | 377 ++++++++++++++++-----
.../plc4x/java/opcua/OpcuaPlcDriverTest.java | 104 +++++-
.../manual/ManualOpcUaS71500NewFWDriverTest.java | 38 +--
.../protocols/knxnetip/knx-master-data.mspec | 5 +-
5 files changed, 402 insertions(+), 128 deletions(-)
(plc4x) 02/02: chore: Updated the KNX Vendor IDs
Posted by cd...@apache.org.
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 015924e6999857e693a676239cd029efd77bca37
Author: Christofer Dutz <ch...@c-ware.de>
AuthorDate: Tue Jul 14 14:25:28 2026 +0200
chore: Updated the KNX Vendor IDs
---
.../org/apache/plc4x/java/knxnetip/readwrite/KnxManufacturer.java | 6 ++++--
.../src/main/generated/protocols/knxnetip/knx-master-data.mspec | 5 +++--
2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/plc4j/drivers/knxnetip/src/main/generated/org/apache/plc4x/java/knxnetip/readwrite/KnxManufacturer.java b/plc4j/drivers/knxnetip/src/main/generated/org/apache/plc4x/java/knxnetip/readwrite/KnxManufacturer.java
index 82b3380792..1a9cda8886 100644
--- a/plc4j/drivers/knxnetip/src/main/generated/org/apache/plc4x/java/knxnetip/readwrite/KnxManufacturer.java
+++ b/plc4j/drivers/knxnetip/src/main/generated/org/apache/plc4x/java/knxnetip/readwrite/KnxManufacturer.java
@@ -1663,9 +1663,11 @@ public enum KnxManufacturer {
M_READYKNX((int) 818, (int) 879, "readyKNX"),
- M_ABB___RESERVED((int) 819, (int) 43954, "ABB - reserved"),
+ M_SMARTVOS((int) 819, (int) 880, "smartVOS"),
- M_BUSCH_JAEGER_ELEKTRO___RESERVED((int) 820, (int) 43959, "Busch-Jaeger Elektro - reserved");
+ M_ABB___RESERVED((int) 820, (int) 43954, "ABB - reserved"),
+
+ M_BUSCH_JAEGER_ELEKTRO___RESERVED((int) 821, (int) 43959, "Busch-Jaeger Elektro - reserved");
private static final Map<Integer, KnxManufacturer> map;
diff --git a/protocols/knxnetip/src/main/generated/protocols/knxnetip/knx-master-data.mspec b/protocols/knxnetip/src/main/generated/protocols/knxnetip/knx-master-data.mspec
index 280bf0f013..aaf1743219 100644
--- a/protocols/knxnetip/src/main/generated/protocols/knxnetip/knx-master-data.mspec
+++ b/protocols/knxnetip/src/main/generated/protocols/knxnetip/knx-master-data.mspec
@@ -1571,8 +1571,9 @@
['816' M_HAAR_INFORMATIK ['877', '"Haar-Informatik"']]
['817' M_DIE_BILDUNGSMANUFAKTUR_GMBH ['878', '"DIE-Bildungsmanufaktur GmbH"']]
['818' M_READYKNX ['879', '"readyKNX"']]
- ['819' M_ABB___RESERVED ['43954', '"ABB - reserved"']]
- ['820' M_BUSCH_JAEGER_ELEKTRO___RESERVED ['43959', '"Busch-Jaeger Elektro - reserved"']]
+ ['819' M_SMARTVOS ['880', '"smartVOS"']]
+ ['820' M_ABB___RESERVED ['43954', '"ABB - reserved"']]
+ ['821' M_BUSCH_JAEGER_ELEKTRO___RESERVED ['43959', '"Busch-Jaeger Elektro - reserved"']]
]
(plc4x) 01/02: 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)
Posted by cd...@apache.org.
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 <ch...@c-ware.de>
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