package at.acdp.opcur.opc; /* * Copyright (c) 2016 Kevin Herron * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * and Eclipse Distribution License v1.0 which accompany this distribution. * * The Eclipse Public License is available at * http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.html. */ import java.lang.reflect.Array; import java.util.List; import java.util.Optional; import java.util.Random; import java.util.UUID; import java.util.concurrent.CompletableFuture; import com.google.common.collect.Lists; import org.eclipse.milo.opcua.sdk.core.AccessLevel; import org.eclipse.milo.opcua.sdk.core.Reference; import org.eclipse.milo.opcua.sdk.core.ValueRank; import org.eclipse.milo.opcua.sdk.server.OpcUaServer; import org.eclipse.milo.opcua.sdk.server.api.AccessContext; import org.eclipse.milo.opcua.sdk.server.api.DataItem; import org.eclipse.milo.opcua.sdk.server.api.MethodInvocationHandler; import org.eclipse.milo.opcua.sdk.server.api.MonitoredItem; import org.eclipse.milo.opcua.sdk.server.api.Namespace; import org.eclipse.milo.opcua.sdk.server.api.nodes.VariableNode; import org.eclipse.milo.opcua.sdk.server.model.nodes.variables.AnalogItemNode; import org.eclipse.milo.opcua.sdk.server.nodes.AttributeContext; import org.eclipse.milo.opcua.sdk.server.nodes.NodeFactory; import org.eclipse.milo.opcua.sdk.server.nodes.ServerNode; import org.eclipse.milo.opcua.sdk.server.nodes.UaDataTypeNode; import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode; import org.eclipse.milo.opcua.sdk.server.nodes.UaMethodNode; import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode; import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectTypeNode; import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode; import org.eclipse.milo.opcua.sdk.server.nodes.delegates.AttributeDelegate; import org.eclipse.milo.opcua.sdk.server.nodes.delegates.AttributeDelegateChain; import org.eclipse.milo.opcua.sdk.server.util.AnnotationBasedInvocationHandler; import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel; import org.eclipse.milo.opcua.stack.core.AttributeId; import org.eclipse.milo.opcua.stack.core.Identifiers; import org.eclipse.milo.opcua.stack.core.StatusCodes; import org.eclipse.milo.opcua.stack.core.UaException; import org.eclipse.milo.opcua.stack.core.types.OpcUaBinaryDataTypeDictionary; import org.eclipse.milo.opcua.stack.core.types.OpcUaDataTypeManager; import org.eclipse.milo.opcua.stack.core.types.builtin.ByteString; import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue; import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime; import org.eclipse.milo.opcua.stack.core.types.builtin.ExtensionObject; import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText; import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName; import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode; import org.eclipse.milo.opcua.stack.core.types.builtin.Variant; import org.eclipse.milo.opcua.stack.core.types.builtin.XmlElement; import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort; import org.eclipse.milo.opcua.stack.core.types.enumerated.NodeClass; import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn; import org.eclipse.milo.opcua.stack.core.types.structured.Range; import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId; import org.eclipse.milo.opcua.stack.core.types.structured.WriteValue; import org.eclipse.milo.opcua.stack.core.util.FutureUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ubyte; import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint; import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ulong; import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ushort; public class MyNamespace implements Namespace { public static final String NAMESPACE_URI = "urn:eclipse:milo:hello-world"; private static final Object[][] STATIC_SCALAR_NODES = new Object[][]{ {"Boolean", Identifiers.Boolean, new Variant(false)}, {"Byte", Identifiers.Byte, new Variant(ubyte(0x00))}, {"SByte", Identifiers.SByte, new Variant((byte) 0x00)}, {"Integer", Identifiers.Integer, new Variant(32)}, {"Int16", Identifiers.Int16, new Variant((short) 16)}, {"Int32", Identifiers.Int32, new Variant(32)}, {"Int64", Identifiers.Int64, new Variant(64L)}, {"UInteger", Identifiers.UInteger, new Variant(uint(32))}, {"UInt16", Identifiers.UInt16, new Variant(ushort(16))}, {"UInt32", Identifiers.UInt32, new Variant(uint(32))}, {"UInt64", Identifiers.UInt64, new Variant(ulong(64L))}, {"Float", Identifiers.Float, new Variant(3.14f)}, {"Double", Identifiers.Double, new Variant(3.14d)}, {"String", Identifiers.String, new Variant("string value")}, {"DateTime", Identifiers.DateTime, new Variant(DateTime.now())}, {"Guid", Identifiers.Guid, new Variant(UUID.randomUUID())}, {"ByteString", Identifiers.ByteString, new Variant(new ByteString(new byte[]{0x01, 0x02, 0x03, 0x04}))}, {"XmlElement", Identifiers.XmlElement, new Variant(new XmlElement("hello"))}, {"LocalizedText", Identifiers.LocalizedText, new Variant(LocalizedText.english("localized text"))}, {"QualifiedName", Identifiers.QualifiedName, new Variant(new QualifiedName(1234, "defg"))}, {"NodeId", Identifiers.NodeId, new Variant(new NodeId(1234, "abcd"))}, {"Duration", Identifiers.Duration, new Variant(1.0)}, {"UtcTime", Identifiers.UtcTime, new Variant(DateTime.now())}, }; private static final Object[][] STATIC_ARRAY_NODES = new Object[][]{ {"BooleanArray", Identifiers.Boolean, false}, {"ByteArray", Identifiers.Byte, ubyte(0)}, {"SByteArray", Identifiers.SByte, (byte) 0x00}, {"Int16Array", Identifiers.Int16, (short) 16}, {"Int32Array", Identifiers.Int32, 32}, {"Int64Array", Identifiers.Int64, 64L}, {"UInt16Array", Identifiers.UInt16, ushort(16)}, {"UInt32Array", Identifiers.UInt32, uint(32)}, {"UInt64Array", Identifiers.UInt64, ulong(64L)}, {"FloatArray", Identifiers.Float, 3.14f}, {"DoubleArray", Identifiers.Double, 3.14d}, {"StringArray", Identifiers.String, "string value"}, {"DateTimeArray", Identifiers.DateTime, DateTime.now()}, {"GuidArray", Identifiers.Guid, UUID.randomUUID()}, {"ByteStringArray", Identifiers.ByteString, new ByteString(new byte[]{0x01, 0x02, 0x03, 0x04})}, {"XmlElementArray", Identifiers.XmlElement, new XmlElement("hello")}, {"LocalizedTextArray", Identifiers.LocalizedText, LocalizedText.english("localized text")}, {"QualifiedNameArray", Identifiers.QualifiedName, new QualifiedName(1234, "defg")}, {"NodeIdArray", Identifiers.NodeId, new NodeId(1234, "abcd")} }; private final Logger logger = LoggerFactory.getLogger(getClass()); private final Random random = new Random(); private final SubscriptionModel subscriptionModel; private final NodeFactory nodeFactory; private final OpcUaServer server; private final UShort namespaceIndex; public MyNamespace(OpcUaServer server, UShort namespaceIndex) { this.server = server; this.namespaceIndex = namespaceIndex; subscriptionModel = new SubscriptionModel(server, this); nodeFactory = new NodeFactory( server.getNodeMap(), server.getObjectTypeManager(), server.getVariableTypeManager() ); try { // Create a "HelloWorld" folder and add it to the node manager NodeId folderNodeId = new NodeId(namespaceIndex, "HelloWorld"); UaFolderNode folderNode = new UaFolderNode( server.getNodeMap(), folderNodeId, new QualifiedName(namespaceIndex, "HelloWorld"), LocalizedText.english("HelloWorld") ); server.getNodeMap().addNode(folderNode); // Make sure our new folder shows up under the server's Objects folder server.getUaNamespace().addReference( Identifiers.ObjectsFolder, Identifiers.Organizes, true, folderNodeId.expanded(), NodeClass.Object ); // Add the rest of the nodes addVariableNodes(folderNode); addMethodNode(folderNode); addCustomDataTypeVariable(folderNode); addCustomObjectTypeAndInstance(folderNode); } catch (UaException e) { logger.error("Error adding nodes: {}", e.getMessage(), e); } } @Override public UShort getNamespaceIndex() { return namespaceIndex; } @Override public String getNamespaceUri() { return NAMESPACE_URI; } private void addVariableNodes(UaFolderNode rootNode) { addArrayNodes(rootNode); addScalarNodes(rootNode); addAdminReadableNodes(rootNode); addAdminWritableNodes(rootNode); addDynamicNodes(rootNode); addDataAccessNodes(rootNode); addWriteOnlyNodes(rootNode); } private void addArrayNodes(UaFolderNode rootNode) { UaFolderNode arrayTypesFolder = new UaFolderNode( server.getNodeMap(), new NodeId(namespaceIndex, "HelloWorld/ArrayTypes"), new QualifiedName(namespaceIndex, "ArrayTypes"), LocalizedText.english("ArrayTypes") ); server.getNodeMap().addNode(arrayTypesFolder); rootNode.addOrganizes(arrayTypesFolder); for (Object[] os : STATIC_ARRAY_NODES) { String name = (String) os[0]; NodeId typeId = (NodeId) os[1]; Object value = os[2]; Object array = Array.newInstance(value.getClass(), 5); for (int i = 0; i < 5; i++) { Array.set(array, i, value); } Variant variant = new Variant(array); UaVariableNode node = new UaVariableNode.UaVariableNodeBuilder(server.getNodeMap()) .setNodeId(new NodeId(namespaceIndex, "HelloWorld/ArrayTypes/" + name)) .setAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE))) .setUserAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE))) .setBrowseName(new QualifiedName(namespaceIndex, name)) .setDisplayName(LocalizedText.english(name)) .setDataType(typeId) .setTypeDefinition(Identifiers.BaseDataVariableType) .setValueRank(ValueRank.OneDimension.getValue()) .setArrayDimensions(new UInteger[]{uint(0)}) .build(); node.setValue(new DataValue(variant)); node.setAttributeDelegate(new ValueLoggingDelegate()); server.getNodeMap().addNode(node); arrayTypesFolder.addOrganizes(node); } } private void addScalarNodes(UaFolderNode rootNode) { UaFolderNode scalarTypesFolder = new UaFolderNode( server.getNodeMap(), new NodeId(namespaceIndex, "HelloWorld/ScalarTypes"), new QualifiedName(namespaceIndex, "ScalarTypes"), LocalizedText.english("ScalarTypes") ); server.getNodeMap().addNode(scalarTypesFolder); rootNode.addOrganizes(scalarTypesFolder); for (Object[] os : STATIC_SCALAR_NODES) { String name = (String) os[0]; NodeId typeId = (NodeId) os[1]; Variant variant = (Variant) os[2]; UaVariableNode node = new UaVariableNode.UaVariableNodeBuilder(server.getNodeMap()) .setNodeId(new NodeId(namespaceIndex, "HelloWorld/ScalarTypes/" + name)) .setAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE))) .setUserAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE))) .setBrowseName(new QualifiedName(namespaceIndex, name)) .setDisplayName(LocalizedText.english(name)) .setDataType(typeId) .setTypeDefinition(Identifiers.BaseDataVariableType) .build(); node.setValue(new DataValue(variant)); node.setAttributeDelegate(new ValueLoggingDelegate()); server.getNodeMap().addNode(node); scalarTypesFolder.addOrganizes(node); } } private void addWriteOnlyNodes(UaFolderNode rootNode) { UaFolderNode writeOnlyFolder = new UaFolderNode( server.getNodeMap(), new NodeId(namespaceIndex, "HelloWorld/WriteOnly"), new QualifiedName(namespaceIndex, "WriteOnly"), LocalizedText.english("WriteOnly") ); server.getNodeMap().addNode(writeOnlyFolder); rootNode.addOrganizes(writeOnlyFolder); String name = "String"; UaVariableNode node = new UaVariableNode.UaVariableNodeBuilder(server.getNodeMap()) .setNodeId(new NodeId(namespaceIndex, "HelloWorld/WriteOnly/" + name)) .setAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.WRITE_ONLY))) .setUserAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.WRITE_ONLY))) .setBrowseName(new QualifiedName(namespaceIndex, name)) .setDisplayName(LocalizedText.english(name)) .setDataType(Identifiers.String) .setTypeDefinition(Identifiers.BaseDataVariableType) .build(); node.setValue(new DataValue(new Variant("can't read this"))); server.getNodeMap().addNode(node); writeOnlyFolder.addOrganizes(node); } private void addAdminReadableNodes(UaFolderNode rootNode) { UaFolderNode adminFolder = new UaFolderNode( server.getNodeMap(), new NodeId(namespaceIndex, "HelloWorld/OnlyAdminCanRead"), new QualifiedName(namespaceIndex, "OnlyAdminCanRead"), LocalizedText.english("OnlyAdminCanRead") ); server.getNodeMap().addNode(adminFolder); rootNode.addOrganizes(adminFolder); String name = "String"; UaVariableNode node = new UaVariableNode.UaVariableNodeBuilder(server.getNodeMap()) .setNodeId(new NodeId(namespaceIndex, "HelloWorld/OnlyAdminCanRead/" + name)) .setAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE))) .setBrowseName(new QualifiedName(namespaceIndex, name)) .setDisplayName(LocalizedText.english(name)) .setDataType(Identifiers.String) .setTypeDefinition(Identifiers.BaseDataVariableType) .build(); node.setValue(new DataValue(new Variant("shh... don't tell the lusers"))); node.setAttributeDelegate(new RestrictedAccessDelegate(identity -> { if ("admin".equals(identity)) { return AccessLevel.READ_WRITE; } else { return AccessLevel.NONE; } })); server.getNodeMap().addNode(node); adminFolder.addOrganizes(node); } private void addAdminWritableNodes(UaFolderNode rootNode) { UaFolderNode adminFolder = new UaFolderNode( server.getNodeMap(), new NodeId(namespaceIndex, "HelloWorld/OnlyAdminCanWrite"), new QualifiedName(namespaceIndex, "OnlyAdminCanWrite"), LocalizedText.english("OnlyAdminCanWrite") ); server.getNodeMap().addNode(adminFolder); rootNode.addOrganizes(adminFolder); String name = "String"; UaVariableNode node = new UaVariableNode.UaVariableNodeBuilder(server.getNodeMap()) .setNodeId(new NodeId(namespaceIndex, "HelloWorld/OnlyAdminCanWrite/" + name)) .setAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE))) .setBrowseName(new QualifiedName(namespaceIndex, name)) .setDisplayName(LocalizedText.english(name)) .setDataType(Identifiers.String) .setTypeDefinition(Identifiers.BaseDataVariableType) .build(); node.setValue(new DataValue(new Variant("admin was here"))); node.setAttributeDelegate(new RestrictedAccessDelegate(identity -> { if ("admin".equals(identity)) { return AccessLevel.READ_WRITE; } else { return AccessLevel.READ_ONLY; } })); server.getNodeMap().addNode(node); adminFolder.addOrganizes(node); } private void addDynamicNodes(UaFolderNode rootNode) { UaFolderNode dynamicFolder = new UaFolderNode( server.getNodeMap(), new NodeId(namespaceIndex, "HelloWorld/Dynamic"), new QualifiedName(namespaceIndex, "Dynamic"), LocalizedText.english("Dynamic") ); server.getNodeMap().addNode(dynamicFolder); rootNode.addOrganizes(dynamicFolder); // Dynamic Boolean { String name = "Boolean"; NodeId typeId = Identifiers.Boolean; Variant variant = new Variant(false); UaVariableNode node = new UaVariableNode.UaVariableNodeBuilder(server.getNodeMap()) .setNodeId(new NodeId(namespaceIndex, "HelloWorld/Dynamic/" + name)) .setAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE))) .setBrowseName(new QualifiedName(namespaceIndex, name)) .setDisplayName(LocalizedText.english(name)) .setDataType(typeId) .setTypeDefinition(Identifiers.BaseDataVariableType) .build(); node.setValue(new DataValue(variant)); AttributeDelegate delegate = AttributeDelegateChain.create( new AttributeDelegate() { @Override public DataValue getValue(AttributeContext context, VariableNode node) throws UaException { return new DataValue(new Variant(random.nextBoolean())); } }, ValueLoggingDelegate::new ); node.setAttributeDelegate(delegate); server.getNodeMap().addNode(node); dynamicFolder.addOrganizes(node); } // Dynamic Int32 { String name = "Int32"; NodeId typeId = Identifiers.Int32; Variant variant = new Variant(0); UaVariableNode node = new UaVariableNode.UaVariableNodeBuilder(server.getNodeMap()) .setNodeId(new NodeId(namespaceIndex, "HelloWorld/Dynamic/" + name)) .setAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE))) .setBrowseName(new QualifiedName(namespaceIndex, name)) .setDisplayName(LocalizedText.english(name)) .setDataType(typeId) .setTypeDefinition(Identifiers.BaseDataVariableType) .build(); node.setValue(new DataValue(variant)); AttributeDelegate delegate = AttributeDelegateChain.create( new AttributeDelegate() { @Override public DataValue getValue(AttributeContext context, VariableNode node) throws UaException { return new DataValue(new Variant(random.nextInt())); } }, ValueLoggingDelegate::new ); node.setAttributeDelegate(delegate); server.getNodeMap().addNode(node); dynamicFolder.addOrganizes(node); } // Dynamic Double { String name = "Double"; NodeId typeId = Identifiers.Double; Variant variant = new Variant(0.0); UaVariableNode node = new UaVariableNode.UaVariableNodeBuilder(server.getNodeMap()) .setNodeId(new NodeId(namespaceIndex, "HelloWorld/Dynamic/" + name)) .setAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE))) .setBrowseName(new QualifiedName(namespaceIndex, name)) .setDisplayName(LocalizedText.english(name)) .setDataType(typeId) .setTypeDefinition(Identifiers.BaseDataVariableType) .build(); node.setValue(new DataValue(variant)); AttributeDelegate delegate = AttributeDelegateChain.create( new AttributeDelegate() { @Override public DataValue getValue(AttributeContext context, VariableNode node) throws UaException { return new DataValue(new Variant(random.nextDouble())); } }, ValueLoggingDelegate::new ); node.setAttributeDelegate(delegate); server.getNodeMap().addNode(node); dynamicFolder.addOrganizes(node); } } private void addDataAccessNodes(UaFolderNode rootNode) { // DataAccess folder UaFolderNode dataAccessFolder = new UaFolderNode( server.getNodeMap(), new NodeId(namespaceIndex, "HelloWorld/DataAccess"), new QualifiedName(namespaceIndex, "DataAccess"), LocalizedText.english("DataAccess") ); server.getNodeMap().addNode(dataAccessFolder); rootNode.addOrganizes(dataAccessFolder); // AnalogItemType node AnalogItemNode node = nodeFactory.createVariable( new NodeId(namespaceIndex, "HelloWorld/DataAccess/AnalogValue"), new QualifiedName(namespaceIndex, "AnalogValue"), LocalizedText.english("AnalogValue"), Identifiers.AnalogItemType, AnalogItemNode.class ); node.setDataType(Identifiers.Double); node.setValue(new DataValue(new Variant(3.14d))); node.setEURange(new Range(0.0, 100.0)); server.getNodeMap().addNode(node); dataAccessFolder.addOrganizes(node); } private void addMethodNode(UaFolderNode folderNode) { UaMethodNode methodNode = UaMethodNode.builder(server.getNodeMap()) .setNodeId(new NodeId(namespaceIndex, "HelloWorld/sqrt(x)")) .setBrowseName(new QualifiedName(namespaceIndex, "sqrt(x)")) .setDisplayName(new LocalizedText(null, "sqrt(x)")) .setDescription( LocalizedText.english("Returns the correctly rounded positive square root of a double value.")) .build(); try { AnnotationBasedInvocationHandler invocationHandler = AnnotationBasedInvocationHandler.fromAnnotatedObject( server.getNodeMap(), new SqrtMethod()); methodNode.setProperty(UaMethodNode.InputArguments, invocationHandler.getInputArguments()); methodNode.setProperty(UaMethodNode.OutputArguments, invocationHandler.getOutputArguments()); methodNode.setInvocationHandler(invocationHandler); server.getNodeMap().addNode(methodNode); folderNode.addReference(new Reference( folderNode.getNodeId(), Identifiers.HasComponent, methodNode.getNodeId().expanded(), methodNode.getNodeClass(), true )); methodNode.addReference(new Reference( methodNode.getNodeId(), Identifiers.HasComponent, folderNode.getNodeId().expanded(), folderNode.getNodeClass(), false )); } catch (Exception e) { logger.error("Error creating sqrt() method.", e); } } private void addCustomObjectTypeAndInstance(UaFolderNode rootFolder) throws UaException { // Define a new ObjectType called "MyObjectType". UaObjectTypeNode objectTypeNode = UaObjectTypeNode.builder(server.getNodeMap()) .setNodeId(new NodeId(namespaceIndex, "ObjectTypes/MyObjectType")) .setBrowseName(new QualifiedName(namespaceIndex, "MyObjectType")) .setDisplayName(LocalizedText.english("MyObjectType")) .setIsAbstract(false) .build(); // "Foo" and "Bar" are members. These nodes are what are called "instance declarations" by the spec. UaVariableNode foo = UaVariableNode.builder(server.getNodeMap()) .setNodeId(new NodeId(namespaceIndex, "ObjectTypes/MyObjectType.Foo")) .setAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE))) .setBrowseName(new QualifiedName(namespaceIndex, "Foo")) .setDisplayName(LocalizedText.english("Foo")) .setDataType(Identifiers.Int16) .setTypeDefinition(Identifiers.BaseDataVariableType) .build(); foo.setValue(new DataValue(new Variant(0))); objectTypeNode.addComponent(foo); UaVariableNode bar = UaVariableNode.builder(server.getNodeMap()) .setNodeId(new NodeId(namespaceIndex, "ObjectTypes/MyObjectType.Bar")) .setAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE))) .setBrowseName(new QualifiedName(namespaceIndex, "Bar")) .setDisplayName(LocalizedText.english("Bar")) .setDataType(Identifiers.String) .setTypeDefinition(Identifiers.BaseDataVariableType) .build(); bar.setValue(new DataValue(new Variant("bar"))); objectTypeNode.addComponent(bar); // Tell the ObjectTypeManager about our new type. // This let's us use NodeFactory to instantiate instances of the type. server.getObjectTypeManager().registerObjectType( objectTypeNode.getNodeId(), UaObjectNode.class, UaObjectNode::new ); // Add our ObjectTypeNode as a subtype of BaseObjectType. server.getUaNamespace().addReference( Identifiers.BaseObjectType, Identifiers.HasSubtype, true, objectTypeNode.getNodeId().expanded(), NodeClass.ObjectType ); // Add the inverse SubtypeOf relationship. objectTypeNode.addReference(new Reference( objectTypeNode.getNodeId(), Identifiers.HasSubtype, Identifiers.BaseObjectType.expanded(), NodeClass.ObjectType, false )); // Add it into the address space. server.getNodeMap().addNode(objectTypeNode); // Use NodeFactory to create instance of MyObjectType called "MyObject". // NodeFactory takes care of recursively instantiating MyObject member nodes // as well as adding all nodes to the address space. UaObjectNode myObject = nodeFactory.createObject( new NodeId(namespaceIndex, "HelloWorld/MyObject"), new QualifiedName(namespaceIndex, "MyObject"), LocalizedText.english("MyObject"), objectTypeNode.getNodeId() ); // Add forward and inverse references from the root folder. rootFolder.addOrganizes(myObject); myObject.addReference(new Reference( myObject.getNodeId(), Identifiers.Organizes, rootFolder.getNodeId().expanded(), rootFolder.getNodeClass(), false )); } private void addCustomDataTypeVariable(UaFolderNode rootFolder) { // add a custom DataTypeNode as a subtype of the built-in Structure DataTypeNode NodeId dataTypeId = new NodeId(namespaceIndex, "DataType.CustomDataType"); UaDataTypeNode dataTypeNode = new UaDataTypeNode( server.getNodeMap(), dataTypeId, new QualifiedName(namespaceIndex, "CustomDataType"), LocalizedText.english("CustomDataType"), LocalizedText.english("CustomDataType"), uint(0), uint(0), false ); // Inverse ref to Structure dataTypeNode.addReference(new Reference( dataTypeId, Identifiers.HasSubtype, Identifiers.Structure.expanded(), NodeClass.DataType, false )); // Forward ref from Structure Optional structureDataTypeNode = server.getNodeMap() .getNode(Identifiers.Structure) .map(UaDataTypeNode.class::cast); structureDataTypeNode.ifPresent(node -> node.addReference(new Reference( node.getNodeId(), Identifiers.HasSubtype, dataTypeId.expanded(), NodeClass.DataType, true )) ); // Create a dictionary, binaryEncodingId, and register the codec under that id OpcUaBinaryDataTypeDictionary dictionary = new OpcUaBinaryDataTypeDictionary( "urn:eclipse:milo:example:custom-data-type" ); NodeId binaryEncodingId = new NodeId(namespaceIndex, "DataType.CustomDataType.BinaryEncoding"); dictionary.registerStructCodec( new CustomDataType.Codec().asBinaryCodec(), "CustomDataType", binaryEncodingId ); // Register dictionary with the shared DataTypeManager instance OpcUaDataTypeManager.getInstance().registerTypeDictionary(dictionary); UaVariableNode customDataTypeVariable = UaVariableNode.builder(server.getNodeMap()) .setNodeId(new NodeId(namespaceIndex, "HelloWorld/CustomDataTypeVariable")) .setAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE))) .setUserAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE))) .setBrowseName(new QualifiedName(namespaceIndex, "CustomDataTypeVariable")) .setDisplayName(LocalizedText.english("CustomDataTypeVariable")) .setDataType(dataTypeId) .setTypeDefinition(Identifiers.BaseDataVariableType) .build(); CustomDataType value = new CustomDataType( "foo", uint(42), true ); ExtensionObject xo = ExtensionObject.encode(value, binaryEncodingId); customDataTypeVariable.setValue(new DataValue(new Variant(xo))); rootFolder.addOrganizes(customDataTypeVariable); customDataTypeVariable.addReference(new Reference( customDataTypeVariable.getNodeId(), Identifiers.Organizes, rootFolder.getNodeId().expanded(), rootFolder.getNodeClass(), false )); } @Override public CompletableFuture> browse(AccessContext context, NodeId nodeId) { ServerNode node = server.getNodeMap().get(nodeId); if (node != null) { return CompletableFuture.completedFuture(node.getReferences()); } else { return FutureUtils.failedFuture(new UaException(StatusCodes.Bad_NodeIdUnknown)); } } @Override public void read( ReadContext context, Double maxAge, TimestampsToReturn timestamps, List readValueIds) { List results = Lists.newArrayListWithCapacity(readValueIds.size()); for (ReadValueId readValueId : readValueIds) { ServerNode node = server.getNodeMap().get(readValueId.getNodeId()); if (node != null) { DataValue value = node.readAttribute( new AttributeContext(context), readValueId.getAttributeId(), timestamps, readValueId.getIndexRange(), readValueId.getDataEncoding() ); results.add(value); } else { results.add(new DataValue(StatusCodes.Bad_NodeIdUnknown)); } } context.complete(results); } @Override public void write(WriteContext context, List writeValues) { List results = Lists.newArrayListWithCapacity(writeValues.size()); for (WriteValue writeValue : writeValues) { ServerNode node = server.getNodeMap().get(writeValue.getNodeId()); if (node != null) { try { node.writeAttribute( new AttributeContext(context), writeValue.getAttributeId(), writeValue.getValue(), writeValue.getIndexRange() ); results.add(StatusCode.GOOD); logger.info( "Wrote value {} to {} attribute of {}", writeValue.getValue().getValue(), AttributeId.from(writeValue.getAttributeId()).map(Object::toString).orElse("unknown"), node.getNodeId()); } catch (UaException e) { logger.error("Unable to write value={}", writeValue.getValue(), e); results.add(e.getStatusCode()); } } else { results.add(new StatusCode(StatusCodes.Bad_NodeIdUnknown)); } } context.complete(results); } @Override public void onDataItemsCreated(List dataItems) { subscriptionModel.onDataItemsCreated(dataItems); } @Override public void onDataItemsModified(List dataItems) { subscriptionModel.onDataItemsModified(dataItems); } @Override public void onDataItemsDeleted(List dataItems) { subscriptionModel.onDataItemsDeleted(dataItems); } @Override public void onMonitoringModeChanged(List monitoredItems) { subscriptionModel.onMonitoringModeChanged(monitoredItems); } @Override public Optional getInvocationHandler(NodeId methodId) { Optional node = server.getNodeMap().getNode(methodId); return node.flatMap(n -> { if (n instanceof UaMethodNode) { return ((UaMethodNode) n).getInvocationHandler(); } else { return Optional.empty(); } }); } }