Repository navigation
Client Nodes
Milo 1.2.0-SNAPSHOT, Java 17. Source baseline 7faaf4f0516c.
So far you have mostly named nodes by NodeId and called services on the client. Node wrappers give you a Java object for each remote node instead, such as a UaObjectNode for Thermostat and a UaVariableNode for Setpoint, with typed getters and remote read and write methods. Methods already used one to call AdjustSetpoint. Using wrappers correctly comes down to knowing when a wrapper answers from its local cache and when it goes to the server. You need a connected client from Connecting and the Thermostat and Setpoint NodeIds, built from the tutorial namespace index as in Your first client.
client.getAddressSpace() returns the client's AddressSpace, which creates wrappers and generated model types for nodes that already exist on the server. A wrapper represents a remote node, so constructing one does not create a node on the server. When AddressSpace builds a wrapper, it reads the node's attributes once and stores them in the wrapper. It then keeps the wrapper in a node cache, so later lookups for the same NodeId return the same instance to every caller while the entry lasts.
Every wrapper method therefore does one of two things. Basic getters and setters such as getDisplayName(), getValue(), and setValue(...) work on the cached copy and send nothing to the server. Methods named read... and write..., together with refresh(...) and synchronize(...), call the server. A cached attribute is only as fresh as the read that filled it.
This method looks up Thermostat and Setpoint, prints the thermostat's name and current setpoint, and then writes a new setpoint.
static void updateSetpoint(
OpcUaClient client, NodeId thermostatId, NodeId setpointId, double newSetpoint)
throws UaException {
AddressSpace addressSpace = client.getAddressSpace();
UaObjectNode thermostat = addressSpace.getObjectNode(thermostatId);
UaVariableNode setpoint = addressSpace.getVariableNode(setpointId);
// The lookup read DisplayName, so this getter answers from the cache.
LocalizedText name = thermostat.getDisplayName();
// readValue() reads from the server and caches the result.
DataValue current = setpoint.readValue();
if (!current.statusCode().isGood()) {
throw new UaException(current.statusCode());
}
System.out.println(name.text() + " setpoint: " + current.value().value());
// writeValue() writes to the server, then updates the cached Value.
setpoint.writeValue(new Variant(newSetpoint));
}Against a freshly started tutorial server, the method prints Thermostat setpoint: 22.0, and Setpoint then holds the new value. readValue() caches its result even when the status is Bad, which is why the method checks the status itself. writeValue(...) throws on a Bad status and otherwise updates the cached Value. If the subscription from Subscriptions is still running, it prints the new value too.
getObjectNode(...) and getVariableNode(...) read the node's required attributes and type information, consult the node cache and type managers, and return the matching wrapper. The lookup fails if the node is missing or has a different NodeClass. Use getNode(nodeId) when you do not know the NodeClass, and the overloads that also take a type-definition NodeId when you already know it. When you have the parent wrapper but not the child's NodeId, thermostat.getVariableComponent("Setpoint") browses Thermostat's HasComponent references for that BrowseName. Like getMethod(String), it assumes the BrowseName is in the parent's namespace.
The node cache belongs to the client. Your application owns any wrappers it keeps and decides when to invalidate or release them. Wrappers have no network connection of their own, so shutting down the parent client when its work is finished is enough.
Standard model wrappers such as ServerTypeNode give named access to model members and properties, and client.getAddressSpace().getServerNode() is a convenient entry point. To get a specialized wrapper for a custom generated type, register that type with the appropriate client type manager first, because a Java cast does not discover a type definition or register a model. Thermostat is a plain BaseObjectType, so its lookup returns a UaObjectNode.
Properties are Variable nodes referenced by their owner, not Java fields on the remote object. Generated property helpers locate and read those nodes, and their exact return types and remote behavior come from the generated API. Keep them distinct from basic cached attribute getters such as getDisplayName().
| Operation on a wrapper | Effect |
|---|---|
Basic attribute getXxx() and setXxx(...)
|
Read or change the local cached attribute. A setter alone does not write remotely. |
readXxx() and writeXxx(...)
|
Perform a remote call. Typed attribute reads and successful attribute writes also update cached attributes, but generated property writes do not refresh their property cache. |
readAttribute(...) and writeAttribute(...)
|
Perform a remote call and return a DataValue or StatusCode for explicit status handling. |
refresh(attributes) |
Read selected attributes and refresh eligible local cached values. |
synchronize(attributes) |
Write selected local values to the server. |
synchronize(attributes) writes selected cached attributes to the server in one Write. It suits code that edits a wrapper's cached attributes first and saves them later, such as a configuration editor. This method stages a new setpoint in the cache, writes it, and refreshes it.
static void synchronizeSetpoint(OpcUaClient client, NodeId setpointId, double newSetpoint)
throws UaException {
UaVariableNode setpoint = client.getAddressSpace().getVariableNode(setpointId);
Set<AttributeId> valueOnly = EnumSet.of(AttributeId.Value);
// Stage the value in the local cache, then write it to the server.
setpoint.setValue(new Variant(newSetpoint));
List<StatusCode> writeResults = setpoint.synchronize(valueOnly);
StatusCode writeStatus = writeResults.get(0);
if (!writeStatus.isGood()) {
throw new UaException(writeStatus);
}
// Read the value back so the cache holds what the server stored.
List<DataValue> readResults = setpoint.refresh(valueOnly);
DataValue refreshed = readResults.get(0);
if (!refreshed.statusCode().isGood()) {
throw new UaException(refreshed.statusCode());
}
}The server value changes only when synchronize runs, so a later setValue(...) by itself leaves the server unchanged. The wrapper is the shared cached instance that every caller of getVariableNode(setpointId) receives, so coordinate concurrent callers before staging or synchronizing values. A multi-attribute synchronization can partially succeed and has no transactional rollback.
refresh and synchronize both return one status per attribute, and a normal return does not prove every operation succeeded, so inspect each one. Unlike readValue(), refresh does not cache a Bad result.
By default, ordinary cache entries expire two minutes after they are written, and the cache holds at most 16,384 entries. node.invalidate() removes the node's entry so the next AddressSpace lookup builds a new wrapper. It does not delete the remote node, and other Java references to the old wrapper keep pointing to it. canonicalize() makes a wrapper the one instance for its NodeId, and that entry stays in the cache until you invalidate it. Use it only when stable wrapper identity is useful, and account for the memory it retains.
Session reconnection does not clear the node cache, so before looking wrappers up again, invalidate those whose namespace mapping or model assumptions changed. Cached wrappers are not live subscriptions. Server configuration changes, permission changes, and reconnection can all invalidate what a cached wrapper tells you, so refresh model metadata when necessary and use Subscriptions for live values. Persist stable namespace URIs and identifiers rather than wrappers or session-specific namespace indexes.
Wrapper lookups throw a UaException when the node is missing or has the wrong NodeClass. Reads and writes differ in how they report a failed operation, so choose the API by how much result detail you need.
| API | Non-Good result |
|---|---|
UaVariableNode.writeValue(...) and its other attribute writers |
Throw UaException for a Bad status. An Uncertain status returns normally and updates the cache. |
Common UaNode attribute writers and most generated model-property writers |
Throw UaException for any non-Good status. |
UaVariableNode.readValue() |
Returns the DataValue without throwing and caches it, even when Bad. |
| Generated model-property readers | Return the payload and discard the status. Use readAttribute(...) or the property node's readValue() when quality matters. |
readAttribute(...), writeAttribute(...), async attribute and property writers, refresh(...), and synchronize(...)
|
Return the DataValue or StatusCode for you to inspect. |
On the tutorial server, writing a String to Setpoint with writeValue(...) throws a UaException with Bad_TypeMismatch, and writing Temperature throws one with Bad_NotWritable. In both cases the cached value stays as it was.
- At this baseline,
UaNode.refreshAsync(Set)andrefresh(Set)read every requested attribute but update the local cache for only the first result. When you need a refreshed wrapper, refresh one attribute at a time, as the synchronize example does. Recheck this on upgrade. - At this baseline, 23 generated writers with multiline signatures still discard the returned status, including
ServerDiagnosticsTypeNode.writeServerDiagnosticsSummary(...). Use the async writer and inspect the StatusCode it returns.
-
ReadNodeExample: Read standard model wrappers against the local ExampleServer.
You have now used every core client operation against the tutorial server's None endpoint. Before you point the client at a real server, continue with Security to give it an application identity and decide which servers it trusts.
Related: Client SDK · Browsing · Reading · Writing
User guide for Milo 1.2.0-SNAPSHOT at source 7faaf4f05, 2026-10-04. Snapshot builds can change after this baseline. Home · Getting started · Examples · Troubleshooting · Release notes
- Home
- Start here
- Client guide
- Server guide
- Feature guides
- Reference
- Operations
- Examples
- Release notes
- Contributing