Skip to content

Server Data Access

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

Expose useful values, quality, timestamps, and write outcomes to clients.

Milo 1.2.0-SNAPSHOT, Java 17. API baseline 7faaf4f0516c.

Values and attributes

Create a Variable as in Address space. Its DataType, ValueRank, and ArrayDimensions describe accepted values. A Java Double is an OPC UA Double; using an Integer with a Double declaration is not an implicit conversion. AccessLevel describes what the Variable supports, and UserAccessLevel describes the current Session's access.

A DataValue contains a Variant, status, and optional source/server timestamps. The source timestamp is when the source produced or observed the value. The server timestamp is the server's processing observation. Preserve source quality and time from the driver; do not manufacture a fresh Good sample when communication failed. Use a bad status such as Bad_CommunicationError when a current value is unavailable, and define whether an old value is retained with degraded quality.

Clients request source, server, both, or neither timestamps. The SDK trims returned timestamps accordingly. ManagedAddressSpace.read() ignores maxAge and returns the stored value. A freshness request does not poll an arbitrary external device; implement the needed cache or device behavior in the address space.

Attribute filters and write validation

UaNode has an attribute filter chain. A filter can supply dynamic reads, validate writes, or expose per-Session effective attributes. Delegate attributes you do not handle. Use readAttribute/writeAttribute when failure must become a UaException status; getAttribute/setAttribute serve ordinary node access too. The server's own direct node setters do not represent an authenticated client operation.

For a scalar Double setpoint that accepts only finite values from 0 through 100, attach this filter to node, a writable UaVariableNode. Import AttributeFilter and AttributeFilterContext from org.eclipse.milo.opcua.sdk.server.nodes.filters, AttributeId, UaException, and StatusCodes from org.eclipse.milo.opcua.stack.core, and DataValue from org.eclipse.milo.opcua.stack.core.types.builtin:

node.getFilterChain()
    .addLast(
        new AttributeFilter() {
          @Override
          public void writeAttribute(
              AttributeFilterContext ctx, AttributeId attributeId, Object value)
              throws UaException {
            if (attributeId == AttributeId.Value) {
              DataValue dataValue = (DataValue) value;
              if (!(dataValue.value().value() instanceof Double setpoint)) {
                throw new UaException(StatusCodes.Bad_TypeMismatch);
              }
              if (!Double.isFinite(setpoint) || setpoint < 0.0 || setpoint > 100.0) {
                throw new UaException(StatusCodes.Bad_OutOfRange);
              }
            }

            ctx.writeAttribute(attributeId, value);
          }
        });

A client write of 35.0 succeeds and changes the stored value. 150.0 returns Bad_OutOfRange without changing it; a String returns Bad_TypeMismatch before this filter receives the Value. This assumes the managed service write path and the Double/scalar declaration. Direct application calls must also honor that declaration and the application policy.

The final ctx.writeAttribute(...) runs the remaining write filters before storage. Calling ctx.setAttribute(...) here would skip their writeAttribute checks. For a device-backed write, replace that storage behavior with a deliberate device command workflow. Decide whether Good means accepted, persisted, or actually applied, and return a status that matches the observable outcome. Do not block a shared worker indefinitely waiting on a controller. The managed AttributeWriter applies numeric index ranges and validates rank and dimensions for array writes. It also stores client-supplied status and timestamps; an absent timestamp is filled with the current time. This filter deliberately validates only the scalar value and accepts that metadata. A write carrying 35.0, bad quality, and a source timestamp therefore stores all three. If device quality and timestamps must remain authoritative, enforce that policy in the write path too. Application node.setValue(...) calls bypass the service write checks. Test partial writes and array behavior separately when exposing them.

Service results and authorization

Read returns one DataValue per requested attribute. Write returns one StatusCode per requested operation. Inspect each even when the overall service request succeeds. A non-writable Value produces Bad_NotWritable; a supported write denied for this Session produces Bad_UserAccessDenied. Type errors are distinct from access errors and application value constraints.

The service layer authorizes before calling AddressSpace.read() or write(). Those lower-level hooks do not repeat authorization. A custom direct caller or sampler must supply the missing checks. Put permissions in effective access attributes or an AccessController, not exclusively in a Value filter: a device sampler may never call that filter.

Live data and cleanup

The default managed sampler reads the same address-space path used by clients. Updating a node's DataValue makes it available to subsequent reads and samples; it does not force a Publish response or bypass data-change filters. For batched device reads or external push sources, use Sampling and keep one producer responsible for each item's samples.

Keep filters fast and thread-safe. Own driver connections, buffers, timers, and pending writes in the namespace lifecycle. Stop delivery and settle or cancel application I/O before dropping its state. A write failure must not silently update the visible value as though the device accepted it.

Examples and reference

Related pages

Server SDK · Sampling · Access control

Clone this wiki locally