package de.opcua.app.opc; import de.opcua.app.model.TreeNodeRef; import org.eclipse.milo.opcua.sdk.client.OpcUaClient; import org.eclipse.milo.opcua.stack.core.Identifiers; import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; import org.eclipse.milo.opcua.stack.core.types.builtin.Variant; import org.eclipse.milo.opcua.stack.core.types.structured.ReferenceDescription; import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; public final class OpcUaService { private volatile OpcUaClient client; // ------------------------------------------------------------ // CONNECTION // ------------------------------------------------------------ public boolean isConnected() { return client != null; } public CompletableFuture connect(String endpointUrl) { try { OpcUaClient c = OpcUaClient.create(endpointUrl); this.client = c; return c.connect().thenApply(ignored -> null); } catch (Exception e) { CompletableFuture failed = new CompletableFuture<>(); failed.completeExceptionally(e); return failed; } } public CompletableFuture disconnect() { OpcUaClient c = client; client = null; if (c == null) return CompletableFuture.completedFuture(null); return c.disconnect().thenApply(ignored -> null); } // ------------------------------------------------------------ // BROWSE — async (für GUI) // ------------------------------------------------------------ public CompletableFuture> browseRoot() { return browseNode(Identifiers.RootFolder); } public CompletableFuture> browse(String nodeId) { return browseNode(NodeId.parse(nodeId)); } private CompletableFuture> browseNode(NodeId nodeId) { try { List refs = client.getAddressSpace().browse(nodeId); return CompletableFuture.completedFuture(mapRefs(refs)); } catch (Exception e) { CompletableFuture> f = new CompletableFuture<>(); f.completeExceptionally(e); return f; } } // ------------------------------------------------------------ // BROWSE — synchron (nur für Digital Twin Build Thread!) // ------------------------------------------------------------ public List browseRootSync() throws Exception { return browseNodeSync(Identifiers.RootFolder); } public List browseSync(String nodeId) throws Exception { return browseNodeSync(NodeId.parse(nodeId)); } private List browseNodeSync(NodeId nodeId) throws Exception { List refs = client.getAddressSpace().browse(nodeId); return mapRefs(refs); } // ── shared mapping ─────────────────────────────────────────────────────── private List mapRefs(List refs) { return refs.stream() .map(r -> { String displayName = r.getDisplayName() != null ? r.getDisplayName().getText() : r.getBrowseName().getName(); String nodeClass = r.getNodeClass() != null ? r.getNodeClass().name() : "Unknown"; String dataType = "Unknown"; try { if (r.getTypeDefinition() != null) dataType = r.getTypeDefinition().toParseableString(); } catch (Exception ignored) {} String referenceTypeId = null; try { if (r.getReferenceTypeId() != null) referenceTypeId = r.getReferenceTypeId().toParseableString(); } catch (Exception ignored) {} return new TreeNodeRef( displayName, r.getNodeId().toParseableString(), r.getBrowseName().getName(), nodeClass, dataType, "ReadWrite", referenceTypeId); }) .collect(Collectors.toList()); } // ------------------------------------------------------------ // READ VALUE — async (für GUI) // ------------------------------------------------------------ public CompletableFuture readValue(String nodeId) { NodeId id = NodeId.parse(nodeId); return client.readValue(0, TimestampsToReturn.Neither, id) .thenApply(dv -> { if (dv == null || dv.getValue() == null) return ""; Object v = dv.getValue().getValue(); return v == null ? "" : v.toString(); }); } // ------------------------------------------------------------ // READ VALUE — synchron (nur für Digital Twin Build Thread!) // ------------------------------------------------------------ public String readValueSync(String nodeId) throws Exception { NodeId id = NodeId.parse(nodeId); var dv = client.readValue(0, TimestampsToReturn.Neither, id) .get(5, TimeUnit.SECONDS); if (dv == null || dv.getValue() == null) return ""; Object v = dv.getValue().getValue(); return v == null ? "" : v.toString(); } // ------------------------------------------------------------ // WRITE VALUE // ------------------------------------------------------------ public CompletableFuture writeValue(String nodeId, Object value) { NodeId id = NodeId.parse(nodeId); try { Variant variant = convertToVariant(value); org.eclipse.milo.opcua.stack.core.types.builtin.DataValue dataValue = new org.eclipse.milo.opcua.stack.core.types.builtin.DataValue(variant); return client.writeValue(id, dataValue) .thenApply(statusCode -> { if (statusCode != null && statusCode.isGood()) { return true; } System.err.println("Write failed for " + nodeId + ": " + statusCode); return false; }) .exceptionally(ex -> { System.err.println("Write exception for " + nodeId + ": " + ex.getMessage()); return false; }); } catch (Exception e) { CompletableFuture failed = new CompletableFuture<>(); failed.completeExceptionally(e); return failed; } } private Variant convertToVariant(Object value) { if (value == null) return new Variant(null); if (value instanceof String) { String str = (String) value; try { return str.contains(".") ? new Variant(Double.parseDouble(str)) : new Variant(Integer.parseInt(str)); } catch (NumberFormatException e) { return new Variant(str); } } else if (value instanceof Integer) return new Variant((Integer) value); else if (value instanceof Long) return new Variant((Long) value); else if (value instanceof Double) return new Variant((Double) value); else if (value instanceof Float) return new Variant((Float) value); else if (value instanceof Boolean) return new Variant((Boolean) value); else if (value instanceof Short) return new Variant((Short) value); else if (value instanceof Byte) return new Variant((Byte) value); return new Variant(value.toString()); } }