Repository navigation
Server Methods
Milo 1.2.0-SNAPSHOT, Java 17. API baseline 7faaf4f0516c.
Data access guarded client writes to Setpoint. The thermostat's other way to change Setpoint is AdjustSetpoint, a Method that adds a Delta and returns the new setpoint as a typed result. This page walks through how the tutorial builds that Method and its handler, then covers complete results, diagnostics, shared Methods, and handler lifetime. Calling methods shows the client side.
A Method is a node, and its behavior lives in a MethodInvocationHandler installed on that node. A CallMethodRequest names both an ObjectId and a MethodId, so the server can check that the Method belongs to that Object, and a discoverable Method cannot be called on an unrelated Object.
When a Call arrives, the service first authorizes it for the Session. The managed address space then finds the Object, finds the Method among the Object's components, and selects the handler for that ObjectId. If the handler extends AbstractMethodInvocationHandler, it checks input count, rank, dimensions, and data type against the declared arguments, then calls your value validation, and only then calls your operation. Each step can end the call with its own status, and the client receives one CallMethodResult per Method call, with a status, per-input results, and outputs.
The tutorial creates the Method node in TutorialNamespace after Thermostat and Setpoint exist, binds a handler that knows the Setpoint node, and makes the Method a component of Thermostat.
UaMethodNode adjustSetpoint =
UaMethodNode.builder(getNodeContext())
.setNodeId(newNodeId("AdjustSetpoint"))
.setBrowseName(newQualifiedName("AdjustSetpoint"))
.setDisplayName(LocalizedText.english("AdjustSetpoint"))
.setDescription(LocalizedText.english("Add Delta to Setpoint and return the result."))
.build();
getNodeManager().addNode(adjustSetpoint);
var handler = new AdjustSetpointHandler(adjustSetpoint, setpoint);
adjustSetpoint.bindInvocationHandler(handler);
thermostat.addComponent(adjustSetpoint);thermostat.addComponent(...) adds the HasComponent reference that makes AdjustSetpoint callable with Thermostat as the ObjectId. bindInvocationHandler(handler) installs the handler with setInvocationHandler(...) and publishes the argument definitions it declares through setInputArguments(...) and setOutputArguments(...), skipping an empty declaration. Clients read the resulting InputArguments and OutputArguments properties to learn the signature, but the handler checks each call against its own declarations, so binding both from one handler keeps them identical. The properties only describe the operation, and the handler implements it. bindInvocationHandler takes an AbstractMethodInvocationHandler, so for a raw MethodInvocationHandler you make those three calls yourself.
The handler declares one Double input, Delta, and one Double output, the new Setpoint. It rejects a non-finite Delta during validation and refuses any adjustment that would leave the 0 to 100 range.
/** Adds Delta to Setpoint when the result stays within 0.0 to 100.0. */
private static final class AdjustSetpointHandler extends AbstractMethodInvocationHandler {
private static final Argument DELTA =
new Argument(
"Delta",
NodeIds.Double,
ValueRanks.Scalar,
null,
LocalizedText.english("The amount to add to the setpoint."));
private static final Argument NEW_SETPOINT =
new Argument(
"Setpoint",
NodeIds.Double,
ValueRanks.Scalar,
null,
LocalizedText.english("The setpoint after the adjustment."));
private final UaVariableNode setpoint;
AdjustSetpointHandler(UaMethodNode node, UaVariableNode setpoint) {
super(node);
this.setpoint = setpoint;
}
@Override
public Argument[] getInputArguments() {
return new Argument[] {DELTA};
}
@Override
public Argument[] getOutputArguments() {
return new Argument[] {NEW_SETPOINT};
}
@Override
protected void validateInputArgumentValues(Variant[] inputs) throws InvalidArgumentException {
// The SDK's type check lets a null value through, so test for a Double here.
boolean finite = inputs[0].value() instanceof Double delta && Double.isFinite(delta);
if (!finite) {
StatusCode outOfRange = new StatusCode(StatusCodes.Bad_OutOfRange);
throw new InvalidArgumentException(new StatusCode[] {outOfRange});
}
}
@Override
protected Variant[] invoke(InvocationContext context, Variant[] inputs) throws UaException {
double delta = (Double) inputs[0].value();
// Writes store Setpoint while holding the node's lock, so holding it here keeps a
// concurrent write or call from changing Setpoint between the read and the update.
synchronized (setpoint) {
double current = (Double) setpoint.getValue().value().value();
double adjusted = current + delta;
boolean inRange = adjusted >= 0.0 && adjusted <= 100.0;
if (!inRange) {
throw new UaException(StatusCodes.Bad_OutOfRange);
}
setpoint.setValue(new DataValue(new Variant(adjusted)));
return new Variant[] {new Variant(adjusted)};
}
}
}The two callbacks split the work by kind of failure. validateInputArgumentValues runs after the SDK's argument checks and judges each input on its own. Use InvalidArgumentException for value constraints. It carries one status per input and optional diagnostic information, and it makes the operation status Bad_InvalidArgument, so the client can tell which input failed. The instanceof test also rejects a null Delta, which passes the SDK's type check because a null value matches any declared type. invoke then decides whether the operation itself can run. It reads, checks, and stores Setpoint while holding the Setpoint node's lock, the same lock a client write holds when it stores a value, so a concurrent write or call cannot change Setpoint between the read and the update. Throwing UaException there makes its status the operation status, with no outputs, and returning output Variants implies Good. The range test is written as adjusted >= 0.0 && adjusted <= 100.0 so that it also fails for NaN, which Setpoint can hold if a client wrote it before the Data access filter was in place.
InvocationContext, the inherited nested type AbstractMethodInvocationHandler.InvocationContext, gives the handler the Session, the ObjectId, and the Method node. The imports are in the complete file on Your first server.
Calling AdjustSetpoint from a client with ObjectId Thermostat and MethodId AdjustSetpoint gives these results.
- With Setpoint at
22.0andDelta1.5, the call returns Good and outputs23.5, and clients subscribed to Setpoint receive23.5. - With
DeltaNaN or infinity, the call returnsBad_InvalidArgument, and the input argument result forDeltaisBad_OutOfRange. - With a
Deltathat would move Setpoint below0.0or above100.0, the call returnsBad_OutOfRangeand Setpoint is unchanged. - With a String
Delta, the SDK's argument check returnsBad_InvalidArgumentwithBad_TypeMismatchas the input argument result, and the handler never runs.
The handler updates Setpoint with setValue(...), which skips the write filter from Data access, so the range check in invoke is the only one that applies to this call.
Override invokeResult(...) instead of invoke(...) when you need to return a complete CallMethodResult, for example Uncertain with useful outputs. A Bad result must not carry outputs, and the result arrays must satisfy the service contract. AbstractMethodInvocationHandler checks this and maps a malformed result to Bad_InternalError. A raw MethodInvocationHandler must enforce those invariants itself. The service replaces null results and statuses and discards malformed argument diagnostics, but it does not apply every invariant.
Override getRequiredInputArgumentCount(...) to let callers omit trailing inputs. Omitted arguments are absent from the input array your handler receives, while an explicit null keeps its position.
For service calls, the invocation context also exposes a DiagnosticsContext. Intern diagnostic strings there and use its indexes in DiagnosticInfo. The Call service applies the client's ReturnDiagnostics mask and assembles the response StringTable, so clients may not receive every diagnostic string. Never emit secret input values.
The service authorizes each call before dispatch, but direct handler calls bypass that check, so code that invokes a handler itself must check access first. Application exceptions, access denial, wrong Object ownership, and invalid arguments each produce a different status, so clients should check the service status and then each Call result.
| Status | Where it comes from | What it means |
|---|---|---|
Bad_UserAccessDenied |
Service authorization | The Session may not call this Method, for example because UserExecutable is false for that user. |
Bad_NodeIdUnknown |
Managed address space | The ObjectId does not exist. |
Bad_MethodInvalid |
Managed address space | The Method is not a component of the named Object, or the ObjectId does not name an Object. |
Bad_ArgumentsMissing |
SDK argument check | Fewer inputs than the required count. The handler does not run. |
Bad_TooManyArguments |
SDK argument check | More inputs than declared. The handler does not run. |
Bad_InvalidArgument |
SDK argument check or validateInputArgumentValues
|
At least one input failed. The input argument results say which and why, such as Bad_TypeMismatch or Bad_OutOfRange. |
Any status thrown from invoke
|
Your handler | The operation failed with that status, such as Bad_OutOfRange from AdjustSetpoint. |
Bad_InternalError |
AbstractMethodInvocationHandler or the address space |
The handler returned a malformed result or threw an exception other than UaException. The server logs both. |
Bad_ResourceUnavailable |
Managed address space | The Session already has the maximum number of Method calls in progress, 64 by default. The JVM system property milo.session.concurrentCallLimit sets the limit. |
A shared Method can serve several Object instances. For ObjectId-specific handlers, use application-owned MethodBindings instead of repeatedly replacing the default handler. bind validates Object ownership and typed argument metadata. Each MethodBinding token owns one registration, so closing an old token cannot remove its replacement. Condition dispatch has its own manager precedence, so use the Condition lifecycle for Condition Methods.
For a UaMethodNode subclass, ManagedAddressSpace Calls use getInvocationHandler(NodeId) and bypass an override of only getInvocationHandler(). Install the handler with setInvocationHandler(...), or override the NodeId overload and customize the handler returned by super.getInvocationHandler(objectId). See method-handler migration.
Close binding tokens or remove Object registrations before deleting owners, and close the registry on namespace shutdown. Cleanup does not delete nodes, cancel in-flight invocations, or wait for callbacks already selected, so keep handler dependencies alive until those calls finish.
Handlers run on server worker threads, and calls from different Sessions, or from separate requests on one Session, can run at the same time, so guard any state that handlers share. AdjustSetpoint reads Setpoint and then writes it, which is enough for a tutorial, but a handler for a real device should apply a relative adjustment as one device operation. Bound device and network work, and design retries around whether the operation is idempotent, because a client timeout does not prove the server skipped the command.
AdjustSetpoint now changes Setpoint and reports every failure with a precise status. To tell clients when Setpoint changes through an event with Thermostat as its source, continue with Events.
Related: Server SDK · Calling methods · Types and instantiation
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