Repository navigation
Server Data Access
Milo 1.2.0-SNAPSHOT, Java 17. API baseline 7faaf4f0516c.
The Setpoint built on Address space accepts any Double, so a client can write 150.0 even though AdjustSetpoint stops at 100.0. This page adds the missing range check, then explains how a Variable presents values, quality, and timestamps, and what clients see when a write fails.
A Variable's Value is a DataValue, which holds a Variant, a status code that tells the client how far to trust the value, and optional source and server timestamps. The source timestamp marks when the source produced or observed the value, and the server timestamp marks when the server processed it. The Variable's DataType, ValueRank, and ArrayDimensions attributes describe the values it accepts. A Java Double is an OPC UA Double, and an Integer is not implicitly converted to one. AccessLevel describes what the Variable supports, and UserAccessLevel the current Session's access.
Values reach a Variable by two paths, and the difference matters for validation. A client Write goes through the service path. The server authorizes it, the managed AttributeWriter checks it against the Variable's declaration, and then the node's attribute filter chain receives it through writeAttribute before storage. Server code that calls a node setter, such as setpoint.setValue(...), goes through the chain's setAttribute instead. That call is not an authenticated client operation and skips the service write checks, so server code must honor the declaration and application policy itself.
The AttributeWriter always checks the value's data type, but it checks rank and dimensions only when ValueRank is 1 or greater. A scalar Variable such as Setpoint gets no shape check, and neither does one declared ScalarOrOneDimension, Any, or OneOrMoreDimensions, so validate shape in a filter when it matters.
You add behavior through the filter chain that each UaNode has. A filter can supply dynamic reads, validate writes, or expose per-Session effective attributes. Use readAttribute and writeAttribute when a failure must become a UaException status, because getAttribute and setAttribute also serve ordinary node access. Delegate attributes you do not handle to the next filter.
This filter limits Setpoint to finite values from 0 through 100. Add it to TutorialNamespace after the code that creates the Variables, where setpoint is the Setpoint UaVariableNode. Imports:
-
AttributeFilterandAttributeFilterContextfromorg.eclipse.milo.opcua.sdk.server.nodes.filters -
AttributeId,UaException, andStatusCodesfromorg.eclipse.milo.opcua.stack.core -
DataValuefromorg.eclipse.milo.opcua.stack.core.types.builtin
setpoint
.getFilterChain()
.addLast(
new AttributeFilter() {
@Override
public void writeAttribute(
AttributeFilterContext ctx, AttributeId attributeId, Object value)
throws UaException {
if (attributeId == AttributeId.Value) {
DataValue dataValue = (DataValue) value;
Object requested = dataValue.value().value();
if (!(requested instanceof Double newSetpoint)) {
throw new UaException(StatusCodes.Bad_TypeMismatch);
}
if (!Double.isFinite(newSetpoint) || newSetpoint < 0.0 || newSetpoint > 100.0) {
throw new UaException(StatusCodes.Bad_OutOfRange);
}
}
ctx.writeAttribute(attributeId, value);
}
});The filter acts only on the Value attribute and passes every other attribute through unchanged. The instanceof check rejects anything that is not a single Double, which also enforces Setpoint's scalar shape. The range check rejects NaN, infinities, and values outside 0 to 100. The exception's status becomes the client's per-operation write result.
The final ctx.writeAttribute(...) runs the remaining write filters and then stores the value. Calling ctx.setAttribute(...) here instead would skip the writeAttribute checks of later filters.
Restart the server and write to Setpoint from a client. A write of 35.0 succeeds, and a client subscribed to Setpoint receives 35.0. A write of 150.0 returns Bad_OutOfRange and leaves Setpoint unchanged. A String fails the AttributeWriter's data type check with Bad_TypeMismatch before the filter runs, while a Double array passes that check and gets Bad_TypeMismatch from the filter.
AdjustSetpoint bypasses the filter, because its handler updates Setpoint with setValue(...), but it checks the same 0 to 100 range itself, as Methods shows. Client writes and AdjustSetpoint calls now enforce the same limits.
Clients trust the status and timestamps you store, so preserve the driver's quality and time, and do not manufacture a fresh Good sample after a communication failure. When no current value is available, use a bad status such as Bad_CommunicationError, and decide whether the old value stays visible with degraded quality.
Client writes carry metadata too. The AttributeWriter stores client-supplied status and timestamps and fills an absent timestamp with the current time. The Setpoint filter validates only the value and accepts that metadata, so a write carrying 35.0, bad quality, and a source timestamp stores all three. If device quality and timestamps must stay authoritative, enforce that in the write path too. The AttributeWriter also applies numeric index ranges, so test partial writes and array behavior separately if you expose them.
On reads, clients request source, server, both, or neither timestamps, and the SDK trims the returned timestamps to match. ManagedAddressSpace.read() ignores maxAge and returns the stored value, so a freshness request does not poll an external device. Implement any cache or device behavior you need in the address space.
For a Variable backed by a device, replace the filter's in-memory storage step with a deliberate device command workflow. Decide whether Good means the device accepted, persisted, or actually applied the value, and return a status that matches the observable outcome. Do not block a shared worker indefinitely waiting on a controller, and never let a failed write update the visible value as though the device accepted it.
The default managed sampler reads through the same address-space path as clients, including the filter chain. Updating a node's DataValue makes the new value available to later reads and samples, and subscribed clients receive it through the normal sampling and publishing cycle, subject to their data-change filters. An update does not force a Publish response. For batched device reads or external push sources, use Sampling, and keep one producer responsible for each item's samples.
The service layer authorizes each operation before it calls AddressSpace.read() or write(), and those lower-level hooks do not repeat the check, so a custom direct caller or sampler must supply its own checks. Put permissions in effective access attributes or an AccessController, as described in Access control, not only in a Value filter, which a device sampler may never call.
Read returns one DataValue per requested attribute, and Write one StatusCode per operation. Inspect each result even when the overall request succeeds, because type errors, access errors, and application value constraints are distinct failures.
| Status | Meaning | What to do |
|---|---|---|
Bad_NotWritable |
The Variable's AccessLevel lacks CurrentWrite, as on Temperature |
Add CurrentWrite if the Variable should accept writes. |
Bad_UserAccessDenied |
The Variable supports writes, but this Session may not write it | Check UserAccessLevel and the access policy for that user. |
Bad_TypeMismatch |
The value's type does not match the DataType, its shape does not match a ValueRank of 1 or greater, or a filter rejected it |
Send a value of the declared type and shape, such as a scalar Double for Setpoint. |
Bad_OutOfRange |
An application filter rejected the value, as the Setpoint filter does outside 0 to 100 | Send a value inside the documented range. |
Bad_CommunicationError in a read value |
The server has no current value from its source | Treat the value as unusable. |
Filters run on server worker threads, so keep them fast and thread-safe. Own driver connections, buffers, timers, and pending writes in the namespace lifecycle. Before dropping that state, stop delivery and settle or cancel application I/O.
Client writes to Setpoint now follow the 0 to 100 rule. To see how AdjustSetpoint enforces the same range and reports its own results, continue with Methods.
Related: Server SDK · Sampling · Access control · Client 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