Skip to content

Server Address Space

Kevin Herron edited this page Oct 12, 2026 · 10 revisions

Milo 1.2.0-RC1, Java 17. API baseline 69cb25087804.

Configuration showed how the server starts and owns the tutorial namespace. This page looks inside that namespace, walks through how the code from Your first server builds the thermostat, and covers the namespaces, stable identities, node classes, references, and types your own model needs.

Nodes, references, and namespaces

An address space is a graph, not a file tree. Each node has a NodeId and a set of attributes, and typed references connect it to other nodes. Clients find your nodes by following references from the standard Objects folder, so storing a node does not by itself give it a useful browse path.

Every NodeId belongs to a namespace, and your nodes live in a ManagedNamespaceWithLifecycle subclass that owns one namespace URI. The tutorial's TutorialNamespace owns urn:eclipse:milo:wiki and builds all four thermostat nodes in its constructor, using getNodeContext(), getNodeManager(), and the helpers newNodeId(...) and newQualifiedName(...).

Build the Thermostat Object

Thermostat is an Object, which groups the parts of one thing in the model. The tutorial creates it with a UaObjectNode.UaObjectNodeBuilder, stores it, and places it under the Objects folder.

UaObjectNode thermostat =
    new UaObjectNode.UaObjectNodeBuilder(getNodeContext())
        .setNodeId(newNodeId("Thermostat"))
        .setBrowseName(newQualifiedName("Thermostat"))
        .setDisplayName(LocalizedText.english("Thermostat"))
        .setTypeDefinition(NodeIds.BaseObjectType)
        .build();
getNodeManager().addNode(thermostat);

thermostat.addReference(
    new Reference(
        thermostat.getNodeId(),
        NodeIds.Organizes,
        NodeIds.ObjectsFolder.expanded(),
        Reference.Direction.INVERSE));

newNodeId("Thermostat") and newQualifiedName("Thermostat") combine the string with the namespace index the server assigned to urn:eclipse:milo:wiki. The builder turns setTypeDefinition(...) into a HasTypeDefinition reference, here to BaseObjectType. addNode makes the node resolvable by NodeId, and the reference makes it reachable.

That reference is an inverse Organizes reference from Thermostat to ObjectsFolder. addReference stores it together with its forward counterpart, and browsing gathers references from every registered node manager, so a client browsing the Objects folder sees Objects Organizes Thermostat.

Add Temperature and Setpoint

Variables hold typed values. The tutorial creates two scalar Double Variables with UaVariableNode.UaVariableNodeBuilder and makes each one a component of Thermostat.

UaVariableNode temperature =
    new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
        .setNodeId(newNodeId("Temperature"))
        .setBrowseName(newQualifiedName("Temperature"))
        .setDisplayName(LocalizedText.english("Temperature"))
        .setDataType(NodeIds.Double)
        .setTypeDefinition(NodeIds.BaseDataVariableType)
        .setAccessLevel(AccessLevel.READ_ONLY)
        .setUserAccessLevel(AccessLevel.READ_ONLY)
        .setValue(new DataValue(new Variant(21.5)))
        .build();
getNodeManager().addNode(temperature);
thermostat.addComponent(temperature);

UaVariableNode setpoint =
    new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
        .setNodeId(newNodeId("Setpoint"))
        .setBrowseName(newQualifiedName("Setpoint"))
        .setDisplayName(LocalizedText.english("Setpoint"))
        .setDataType(NodeIds.Double)
        .setTypeDefinition(NodeIds.BaseDataVariableType)
        .setAccessLevel(AccessLevel.READ_WRITE)
        .setUserAccessLevel(AccessLevel.READ_WRITE)
        .setValue(new DataValue(new Variant(22.0)))
        .build();
getNodeManager().addNode(setpoint);
thermostat.addComponent(setpoint);

setDataType(NodeIds.Double) declares the values each Variable accepts. The builder's default ValueRank is scalar, so the tutorial does not set it. Their access attributes differ. Temperature sets both AccessLevel and UserAccessLevel to READ_ONLY, which is how you expose a read-only device value, while Setpoint allows reads and writes. User-dependent access needs an explicit policy, described in Access control. thermostat.addComponent(...) adds a HasComponent reference from Thermostat to the Variable, together with its inverse.

Run the server and browse it with any OPC UA client using anonymous identity and SecurityPolicy None. Browsing Objects finds Thermostat, and browsing Thermostat finds Temperature, Setpoint, and AdjustSetpoint through HasComponent. A browse path from Objects through Organizes to Thermostat and then HasComponent to Setpoint resolves to the Setpoint node. Reads and writes return the results listed in Your first server.

Stable identities

Clients and configuration files keep NodeIds, so choose identities that last. Persist the namespace URI and identifier, not the numeric namespace index assigned by one server run. Within a namespace, newNodeId(...) and newQualifiedName(...) use the current index, which is why the tutorial passes plain strings such as "Setpoint". Choose identifiers that survive application restarts and model reorganization. Browse names locate nodes within a model, but they do not replace a stable NodeId.

Objects, Methods, and types

Objects group components, Variables hold typed values, and Methods are operations on an owning Object. AdjustSetpoint is a Method component of Thermostat. A Call request names both the Object and the Method, and the Method must belong to the named Object. Methods shows how the tutorial builds AdjustSetpoint and its handler.

References say how nodes relate. Organizes describes organization, as between the Objects folder and Thermostat, while HasComponent and HasProperty express model relationships, as between Thermostat and its parts. A type definition points to an ObjectType or VariableType, and HasSubtype relates types. Explicit event notifier hierarchies need paired references, as described in Events.

A type node describes structure but does not implement application behavior by itself. The thermostat uses the base types and adds its components by hand. Use Types and instantiation when a reusable type supplies children, mandatory or optional members, or Methods. The SDK has no general NodeSet2 importer, and generated standard model classes and XML value decoding do not import an arbitrary companion specification.

Namespace and lifecycle

A ManagedNamespaceWithLifecycle constructor registers the namespace URI in the server's namespace table and obtains an index. Startup registers the namespace and its node manager with the address space, and shutdown removes them. When you pass the namespace to server.addLifecycleParticipant(namespace) before startup, as shown in Configuration, the server runs both steps for you.

The tutorial builds its nodes in the constructor because they need no other resources. When node construction depends on a device connection or other resources, add that work and its cleanup through getLifecycleManager(), so it runs at startup and is undone at shutdown. The inherited sampling manager starts after the namespace's own lifecycle work and stops before it. Shut down device work and callbacks before releasing their state.

A managed namespace combines an AddressSpace, which handles service operations, with a UaNodeManager, which stores managed nodes and references, and a namespace-based filter routes operations for its namespace to it. It serves only nodes that exist in its node manager. A custom address space can serve virtual nodes instead, but it must then implement consistent routing, reads, browsing, results, and monitored-item lifecycle. An in-memory node manager does not by itself provide node-management service support.

When things go wrong

Reading an unknown node returns Bad_NodeIdUnknown. Wrong data types, missing references, and a namespace that never started are separate failures, so inspect operation results and browse the expected parent to tell them apart. A node that reads by NodeId but is missing from browse results usually lacks a reference from its parent. Every public NodeId must resolve through the address space that claims it.

Node lifetime and cleanup

The application owns node lifetime. Remove Method bindings, Condition registrations, and external callbacks before deleting their nodes. node.delete() removes the node and its owned relationships according to the node API, but it does not dispose of drivers or timers.

For a model created by NodeInstantiator, keep its result and call deleteCreated() to remove exactly the recorded additions. Unregistering a namespace makes its nodes inaccessible, while application storage and device resources still need their own cleanup.

Examples and reference

Next steps

The thermostat's nodes exist, but Setpoint accepts any Double a client writes. To validate writes and serve values with the right quality and timestamps, continue with Data access.

Related: Server SDK · Types and instantiation · OPC UA concepts · Client browsing

Clone this wiki locally