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 2631a7ccb6 feat: Added browse support for PLC4J OPC-UA
2631a7ccb6 is described below
commit 2631a7ccb65dc75911e306bd9be133e2a1f89750
Author: Christofer Dutz <[email protected]>
AuthorDate: Mon Jul 13 16:33:18 2026 +0200
feat: Added browse support for PLC4J OPC-UA
---
.../apache/plc4x/java/opcua/OpcuaConnection.java | 209 ++++++++++++++++++++-
.../apache/plc4x/java/opcua/OpcuaPlcDriver.java | 5 +
.../plc4x/java/opcua/tag/OpcuaPlcTagHandler.java | 3 +-
.../{OpcuaPlcTagHandler.java => OpcuaQuery.java} | 26 ++-
.../plc4x/java/opcua/OpcuaPlcDriverTest.java | 64 +++++++
.../opcua/manual/ManualOpcUaS71500NewFWBrowse.java | 84 +++++++++
6 files changed, 375 insertions(+), 16 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 ddd84e6d3a..d2612e9fad 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
@@ -27,18 +27,17 @@ import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
import org.apache.plc4x.java.api.authentication.PlcAuthentication;
import
org.apache.plc4x.java.api.authentication.PlcUsernamePasswordAuthentication;
import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
import org.apache.plc4x.java.api.messages.*;
-import org.apache.plc4x.java.api.metadata.Metadata;
-import org.apache.plc4x.java.api.metadata.time.TimeSource;
import org.apache.plc4x.java.api.model.PlcConsumerRegistration;
+import org.apache.plc4x.java.api.model.PlcQuery;
import org.apache.plc4x.java.api.model.PlcSubscriptionHandle;
import org.apache.plc4x.java.api.model.PlcTag;
import org.apache.plc4x.java.api.types.PlcResponseCode;
+import org.apache.plc4x.java.api.types.PlcSubscriptionType;
import org.apache.plc4x.java.api.types.PlcValueType;
import org.apache.plc4x.java.api.value.PlcValue;
import org.apache.plc4x.java.opcua.config.OpcuaConfiguration;
@@ -49,9 +48,8 @@ import org.apache.plc4x.java.opcua.context.SecureChannel;
import org.apache.plc4x.java.opcua.protocol.OpcuaSubscriptionHandle;
import org.apache.plc4x.java.opcua.readwrite.*;
import org.apache.plc4x.java.opcua.tag.OpcuaPlcTagHandler;
-import org.apache.plc4x.java.opcua.tag.OpcuaQualityStatus;
+import org.apache.plc4x.java.opcua.tag.OpcuaQuery;
import org.apache.plc4x.java.opcua.tag.OpcuaTag;
-import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException;
import org.apache.plc4x.java.spi.drivers.ConnectionBase;
import org.apache.plc4x.java.spi.drivers.exceptions.MessageCodecException;
import org.apache.plc4x.java.spi.drivers.tags.PlcTagHandler;
@@ -74,6 +72,7 @@ import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Predicate;
+import java.util.stream.Collectors;
public class OpcuaConnection extends ConnectionBase<OpcuaConfiguration>
implements OpcuaWire {
@@ -374,6 +373,206 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
return response;
}
+ // The standard "Objects" folder — the usual entry point into a server's
address space.
+ private static final String OBJECTS_FOLDER_ADDRESS = "ns=0;i=85";
+ // HierarchicalReferences (i=33): browsing only these (plus subtypes)
yields the clean
+ // containment tree (Organizes / HasComponent / HasProperty, ...) instead
of every
+ // reference type (which would drag in type definitions and other
non-containment noise).
+ private static final NodeId HIERARCHICAL_REFERENCES = new NodeId(new
NodeIdNumeric(0, 33L));
+ // resultMask 0x3F -> return all reference fields (referenceType,
isForward, nodeClass,
+ // browseName, displayName, typeDefinition).
+ private static final long BROWSE_RESULT_MASK_ALL = 0x3FL;
+
+ @Override
+ protected CompletableFuture<PlcBrowseResponse> onBrowse(PlcBrowseRequest
browseRequest) {
+ return onBrowseWithInterceptor(browseRequest, (queryName, query, item)
-> true);
+ }
+
+ @Override
+ protected CompletableFuture<PlcBrowseResponse>
onBrowseWithInterceptor(PlcBrowseRequest browseRequest,
+
PlcBrowseRequestInterceptor interceptor) {
+ Map<String, PlcResponseCode> responseCodes = new ConcurrentHashMap<>();
+ Map<String, List<PlcBrowseItem>> values = new ConcurrentHashMap<>();
+ List<CompletableFuture<?>> queryFutures = new ArrayList<>();
+
+ for (String queryName : browseRequest.getQueryNames()) {
+ PlcQuery query = browseRequest.getQuery(queryName);
+ String startAddress = query.getQueryString();
+ // An empty query or the "**" wildcard means "browse everything":
start at the
+ // standard Objects folder and recurse the whole sub-tree.
Otherwise the query is
+ // the address of the node to start browsing from.
+ if (startAddress == null || startAddress.isBlank() ||
"**".equals(startAddress.trim())) {
+ startAddress = OBJECTS_FOLDER_ADDRESS;
+ }
+ NodeId startNodeId;
+ try {
+ startNodeId = generateNodeId(OpcuaTag.of(startAddress));
+ } catch (Exception e) {
+ LOGGER.warn("Invalid browse start node '{}' for query '{}'",
startAddress, queryName, e);
+ responseCodes.put(queryName, PlcResponseCode.INVALID_ADDRESS);
+ values.put(queryName, Collections.emptyList());
+ continue;
+ }
+ // A shared, global set of already-visited nodes provides cycle
detection (a node
+ // is never expanded twice) and keeps a well-formed tree from
being duplicated.
+ Set<String> visited = ConcurrentHashMap.newKeySet();
+ queryFutures.add(
+ browseNode(startNodeId, visited, queryName, query, interceptor)
+ .handle((items, error) -> {
+ if (error != null) {
+ LOGGER.warn("Browsing failed for query '{}'",
queryName, error);
+ responseCodes.put(queryName,
PlcResponseCode.INTERNAL_ERROR);
+ values.put(queryName, Collections.emptyList());
+ } else {
+ responseCodes.put(queryName, PlcResponseCode.OK);
+ values.put(queryName, items);
+ }
+ return null;
+ }));
+ }
+
+ return CompletableFuture.allOf(queryFutures.toArray(new
CompletableFuture[0]))
+ .thenApply(v -> new DefaultPlcBrowseResponse(browseRequest,
responseCodes, values));
+ }
+
+ /**
+ * Browses the forward hierarchical references of {@code nodeId} and
returns each referenced
+ * node as a {@link PlcBrowseItem}, recursing into every not-yet-visited
child so the full
+ * sub-tree is returned. {@code visited} guards against reference cycles.
+ */
+ private CompletableFuture<List<PlcBrowseItem>> browseNode(NodeId nodeId,
Set<String> visited, String queryName,
+ PlcQuery query,
PlcBrowseRequestInterceptor interceptor) {
+ return browseReferences(nodeId, null, new
ArrayList<>()).thenCompose(references -> {
+ List<CompletableFuture<PlcBrowseItem>> itemFutures = new
ArrayList<>();
+ for (ReferenceDescription reference : references) {
+ String childAddress = addressOf(reference.getNodeId());
+ if (childAddress == null) {
+ // Node identifier form we can't turn back into an address
(e.g. opaque) — skip.
+ continue;
+ }
+ CompletableFuture<Map<String, PlcBrowseItem>> childrenFuture;
+ if (visited.add(childAddress)) {
+ childrenFuture =
browseNode(generateNodeId(OpcuaTag.of(childAddress)),
+ visited, queryName, query, interceptor)
+ .thenApply(childItems -> childItems.stream()
+ .collect(Collectors.toMap(PlcBrowseItem::getName,
item -> item,
+ (a, b) -> a, LinkedHashMap::new)));
+ } else {
+ // Already visited (cycle or shared node) — list it but
don't expand again.
+ childrenFuture =
CompletableFuture.completedFuture(Collections.emptyMap());
+ }
+ itemFutures.add(childrenFuture.thenApply(children ->
buildBrowseItem(reference, childAddress, children)));
+ }
+ return CompletableFuture.allOf(itemFutures.toArray(new
CompletableFuture[0]))
+ .thenApply(v -> itemFutures.stream()
+ .map(CompletableFuture::join)
+ .filter(item -> interceptor.intercept(queryName, query,
item))
+ .collect(Collectors.toList()));
+ });
+ }
+
+ /**
+ * Issues a single Browse (or BrowseNext when {@code continuationPoint} is
set) and follows
+ * continuation points until the server has returned all references of the
node.
+ */
+ private CompletableFuture<List<ReferenceDescription>>
browseReferences(NodeId nodeId, PascalByteString continuationPoint,
+
List<ReferenceDescription> accumulator) {
+ CompletableFuture<BrowseResult> resultFuture;
+ if (continuationPoint == null) {
+ BrowseDescription description = new BrowseDescription(
+ nodeId,
+ BrowseDirection.browseDirectionForward,
+ HIERARCHICAL_REFERENCES,
+ true, // include reference subtypes
+ 0L, // nodeClassMask 0 -> all node classes
+ BROWSE_RESULT_MASK_ALL);
+ BrowseRequest request = new BrowseRequest(
+ conversation.createRequestHeader(),
+ new ViewDescription(new NodeId(new NodeIdTwoByte((short) 0)),
0L, 0L),
+ 0L, // requestedMaxReferencesPerNode 0 ->
server decides
+ Collections.singletonList(description));
+ resultFuture = conversation.submit(request, BrowseResponse.class)
+ .thenApply(response -> response.getResults().get(0));
+ } else {
+ BrowseNextRequest request = new BrowseNextRequest(
+ conversation.createRequestHeader(),
+ false, // don't release the continuation point,
we want the next batch
+ Collections.singletonList(continuationPoint));
+ resultFuture = conversation.submit(request,
BrowseNextResponse.class)
+ .thenApply(response -> response.getResults().get(0));
+ }
+ return resultFuture.thenCompose(result -> {
+ if (result.getReferences() != null) {
+ accumulator.addAll(result.getReferences());
+ }
+ PascalByteString nextContinuationPoint =
result.getContinuationPoint();
+ if (nextContinuationPoint != null &&
nextContinuationPoint.getStringLength() > 0) {
+ return browseReferences(nodeId, nextContinuationPoint,
accumulator);
+ }
+ return CompletableFuture.completedFuture(accumulator);
+ });
+ }
+
+ private PlcBrowseItem buildBrowseItem(ReferenceDescription reference,
String address, Map<String, PlcBrowseItem> children) {
+ OpcuaTag tag = OpcuaTag.of(address);
+ NodeClass nodeClass = reference.getNodeClass();
+ boolean isVariable = nodeClass == NodeClass.nodeClassVariable;
+ String name = localizedTextValue(reference.getDisplayName());
+ if (name == null || name.isEmpty()) {
+ name = qualifiedNameValue(reference.getBrowseName());
+ }
+
+ Map<String, PlcValue> options = new HashMap<>();
+ if (nodeClass != null) {
+ options.put("node-class", new PlcSTRING(nodeClass.name()));
+ }
+ String browseName = qualifiedNameValue(reference.getBrowseName());
+ if (browseName != null) {
+ options.put("browse-name", new PlcSTRING(browseName));
+ }
+
+ // Phase 1: only variable nodes are readable/writable. The actual
access rights (and the
+ // data type) are refined once server-side type resolution is wired in.
+ return new DefaultPlcBrowseItem(tag, name, isVariable, isVariable,
+ Collections.<PlcSubscriptionType>emptySet(), false,
Collections.emptyList(), children, options);
+ }
+
+ /** Formats the target node of a reference back into an OpcuaTag address
(or null if unsupported). */
+ private static String addressOf(ExpandedNodeId expandedNodeId) {
+ if (expandedNodeId == null) {
+ return null;
+ }
+ NodeIdTypeDefinition nodeId = expandedNodeId.getNodeId();
+ String identifier = nodeId.getIdentifier();
+ if (nodeId instanceof NodeIdTwoByte) {
+ return "ns=0;i=" + identifier;
+ } else if (nodeId instanceof NodeIdFourByte fourByte) {
+ return "ns=" + fourByte.getNamespaceIndex() + ";i=" + identifier;
+ } else if (nodeId instanceof NodeIdNumeric numeric) {
+ return "ns=" + numeric.getNamespaceIndex() + ";i=" + identifier;
+ } else if (nodeId instanceof NodeIdString string) {
+ return "ns=" + string.getNamespaceIndex() + ";s=" + identifier;
+ }
+ // GUID and opaque (ByteString) node identifiers are not round-tripped
in Phase 1:
+ // NodeIdGuid#getIdentifier() returns the raw byte-array toString(),
which the tag
+ // parser can't turn back into a usable node id. Such nodes are
skipped for now.
+ return null;
+ }
+
+ private static String localizedTextValue(LocalizedText localizedText) {
+ if (localizedText == null || !localizedText.getTextSpecified() ||
localizedText.getText() == null) {
+ return null;
+ }
+ return localizedText.getText().getStringValue();
+ }
+
+ private static String qualifiedNameValue(QualifiedName qualifiedName) {
+ if (qualifiedName == null || qualifiedName.getName() == null) {
+ return null;
+ }
+ return qualifiedName.getName().getStringValue();
+ }
+
public static PlcValue variantToPlcValue(PlcTag tag, Variant variant) {
PlcValue value = null;
if (variant instanceof VariantBoolean) {
diff --git
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaPlcDriver.java
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaPlcDriver.java
index fc1747a1fa..051fee7d34 100644
---
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaPlcDriver.java
+++
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaPlcDriver.java
@@ -71,6 +71,11 @@ public class OpcuaPlcDriver extends DriverBase {
return true;
}
+ @Override
+ protected boolean canBrowse() {
+ return true;
+ }
+
@Override
protected ConnectionBase<?> getConnection(Configuration configuration,
TransportInstance<?>
transportInstance,
diff --git
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaPlcTagHandler.java
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaPlcTagHandler.java
index 0dfb984644..0e85d0ec95 100644
---
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaPlcTagHandler.java
+++
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaPlcTagHandler.java
@@ -34,7 +34,8 @@ public class OpcuaPlcTagHandler implements PlcTagHandler {
@Override
public PlcQuery parseQuery(String query) {
- throw new UnsupportedOperationException("This driver doesn't support
browsing");
+ // The query is the address of the node to start browsing from (empty
=> Objects folder).
+ return new OpcuaQuery(query);
}
}
diff --git
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaPlcTagHandler.java
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaQuery.java
similarity index 63%
copy from
plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaPlcTagHandler.java
copy to
plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaQuery.java
index 0dfb984644..5996ca225a 100644
---
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaPlcTagHandler.java
+++
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaQuery.java
@@ -18,23 +18,29 @@
*/
package org.apache.plc4x.java.opcua.tag;
-import org.apache.plc4x.java.api.exceptions.PlcInvalidTagException;
import org.apache.plc4x.java.api.model.PlcQuery;
-import org.apache.plc4x.java.spi.drivers.tags.PlcTagHandler;
-public class OpcuaPlcTagHandler implements PlcTagHandler {
+/**
+ * A browse query for the OPC UA driver. The query string is the address of
the node to
+ * start browsing from (e.g. {@code ns=2;s=HelloWorld}); an empty query starts
at the
+ * standard {@code Objects} folder.
+ */
+public class OpcuaQuery implements PlcQuery {
+
+ private final String queryString;
+
+ public OpcuaQuery(String queryString) {
+ this.queryString = queryString;
+ }
@Override
- public OpcuaTag parseTag(String tagAddress) {
- if (OpcuaTag.matches(tagAddress)) {
- return OpcuaTag.of(tagAddress);
- }
- throw new PlcInvalidTagException(tagAddress);
+ public String getQueryString() {
+ return queryString;
}
@Override
- public PlcQuery parseQuery(String query) {
- throw new UnsupportedOperationException("This driver doesn't support
browsing");
+ public String toString() {
+ return "OpcuaQuery{" + queryString + '}';
}
}
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 492451ba09..3ce07f31fa 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
@@ -212,6 +212,70 @@ public class OpcuaPlcDriverTest {
connectionStringValidSet = List.of(tcpConnectionAddress);
}
+ @Test
+ void browseWildcardDiscoversWholeAddressSpace() throws Exception {
+ try (PlcConnection connection = new
DefaultPlcDriverManager().getConnection(tcpConnectionAddress)) {
+ // "**" is the "browse everything" wildcard: it starts at the
Objects folder and
+ // recurses the whole sub-tree.
+ org.apache.plc4x.java.api.messages.PlcBrowseResponse response =
connection.browseRequestBuilder()
+ .addQuery("all", "**")
+ .build().execute().get(60, TimeUnit.SECONDS);
+
assertThat(response.getResponseCode("all")).isEqualTo(PlcResponseCode.OK);
+
+ java.util.List<org.apache.plc4x.java.api.messages.PlcBrowseItem>
top = response.getValues("all");
+ // The example server exposes the 'HelloWorld' folder under
Objects.
+
assertThat(top.stream().map(org.apache.plc4x.java.api.messages.PlcBrowseItem::getName)).contains("HelloWorld");
+
+ java.util.List<org.apache.plc4x.java.api.messages.PlcBrowseItem>
all = new ArrayList<>();
+ collectBrowseItems(top, all);
+ assertThat(all.size()).isGreaterThan(50);
+ }
+ }
+
+ @Test
+ void browseDiscoversAddressSpace() throws Exception {
+ try (PlcConnection connection = new
DefaultPlcDriverManager().getConnection(tcpConnectionAddress)) {
+ assertThat(connection.getMetadata().isBrowseSupported()).isTrue();
+
+ org.apache.plc4x.java.api.messages.PlcBrowseResponse response =
connection.browseRequestBuilder()
+ .addQuery("hw", "ns=2;s=HelloWorld")
+ .build().execute().get(30, TimeUnit.SECONDS);
+
assertThat(response.getResponseCode("hw")).isEqualTo(PlcResponseCode.OK);
+
+ java.util.List<org.apache.plc4x.java.api.messages.PlcBrowseItem>
top = response.getValues("hw");
+ assertThat(top).isNotEmpty();
+
+ // 'ScalarTypes' is an Object (container) node: discovered, has
children, not readable.
+ org.apache.plc4x.java.api.messages.PlcBrowseItem scalarTypes =
top.stream()
+ .filter(i ->
"ScalarTypes".equals(i.getName())).findFirst().orElse(null);
+ assertThat(scalarTypes).as("ScalarTypes folder").isNotNull();
+ assertThat(scalarTypes.isReadable()).isFalse();
+ assertThat(scalarTypes.getChildren()).isNotEmpty();
+
+ // A concrete variable underneath it is readable and writable,
with a usable address.
+ org.apache.plc4x.java.api.messages.PlcBrowseItem boolVar =
scalarTypes.getChildren().values().stream()
+ .filter(i ->
"Boolean".equals(i.getName())).findFirst().orElse(null);
+ assertThat(boolVar).as("ScalarTypes/Boolean variable").isNotNull();
+ assertThat(boolVar.isReadable()).isTrue();
+ assertThat(boolVar.isWritable()).isTrue();
+
assertThat(boolVar.getTag().getAddressString()).contains("ns=2;s=HelloWorld/ScalarTypes/Boolean");
+
+ // Full-subtree recursion reached deeply-nested property nodes
(AnalogValue -> EURange).
+ java.util.List<org.apache.plc4x.java.api.messages.PlcBrowseItem>
all = new ArrayList<>();
+ collectBrowseItems(top, all);
+ assertThat(all).anyMatch(i -> "EURange".equals(i.getName()));
+ assertThat(all.size()).isGreaterThan(40);
+ }
+ }
+
+ 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) {
+ out.add(item);
+ collectBrowseItems(new ArrayList<>(item.getChildren().values()),
out);
+ }
+ }
+
@Nested
class SmokeTest {
@Test
diff --git
a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/manual/ManualOpcUaS71500NewFWBrowse.java
b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/manual/ManualOpcUaS71500NewFWBrowse.java
new file mode 100644
index 0000000000..e09b7ec533
--- /dev/null
+++
b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/manual/ManualOpcUaS71500NewFWBrowse.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.opcua.manual;
+
+import org.apache.plc4x.java.api.PlcConnection;
+import org.apache.plc4x.java.api.PlcDriverManager;
+import org.apache.plc4x.java.api.messages.PlcBrowseItem;
+import org.apache.plc4x.java.api.messages.PlcBrowseResponse;
+import org.apache.plc4x.java.api.model.PlcQuery;
+import org.apache.plc4x.java.api.types.PlcResponseCode;
+
+import java.util.stream.Collectors;
+
+public class ManualOpcUaS71500NewFWBrowse {
+
+ public static void main(String[] args) throws Exception {
+
+ long startTime = System.currentTimeMillis();
+ try (PlcConnection connection =
PlcDriverManager.getDefault().getConnectionManager().getConnection("opcua://192.168.24.66:4840")){
+ PlcBrowseResponse plcBrowseResponse =
connection.browseRequestBuilder()
+ .addQuery("all", "**")
+ .build().executeWithInterceptor((queryName, query, item) -> {
+ outputItem(queryName, query, item, 0);
+ return true;
+ }).get();
+
+ long endTime = System.currentTimeMillis();
+ int numNodes = 0;
+ for (String queryName : plcBrowseResponse.getQueryNames()) {
+ if (plcBrowseResponse.getResponseCode(queryName) !=
PlcResponseCode.OK) {
+ continue;
+ }
+
+ for (PlcBrowseItem value :
plcBrowseResponse.getValues(queryName)) {
+ numNodes += countNodes(value);
+ }
+ }
+ System.out.printf("Took %dms returned %d nodes%n", endTime -
startTime, numNodes);
+ }
+ }
+
+ protected static void outputItem(String queryName, PlcQuery query,
PlcBrowseItem item, int level) {
+ System.out.printf("%s- %s: name: %s address: %s - type: %s%s%s%n",
+ " ".repeat(level),
+ queryName,
+ item.getName(),
+ item.getTag().getAddressString(),
+ item.getTag().getPlcValueType(),
+ (item.getArrayInformation() != null) &&
!item.getArrayInformation().isEmpty() ? " " +
item.getArrayInformation().stream().map(arrayInfo -> "[" +
arrayInfo.getLowerBound() + ".." + arrayInfo.getUpperBound() +
"]").collect(Collectors.joining()) : "",
+ (item.getOptions() != null) && !item.getOptions().isEmpty() ? " {"
+ item.getOptions().entrySet().stream().map(stringPlcValueEntry ->
stringPlcValueEntry.getKey() + ": \"" +
stringPlcValueEntry.getValue().toString() + "\"").collect(Collectors.joining(",
")) + "}" : "");
+ if ((item.getChildren() != null) && !item.getChildren().isEmpty()) {
+ item.getChildren().forEach((s, plcBrowseItem) ->
outputItem(queryName, query, plcBrowseItem, level + 1));
+ }
+ }
+
+ protected static int countNodes(PlcBrowseItem browseItem) {
+ if(browseItem.getChildren().isEmpty()) {
+ return 1;
+ }
+
+ int nodes = 1;
+ for (PlcBrowseItem childBrowseItem :
browseItem.getChildren().values()) {
+ nodes += countNodes(childBrowseItem);
+ }
+ return nodes;
+ }
+
+}