Raw services & errors¶
The raw service interface¶
When the high-level call does not expose a parameter you need, drop to the
service layer. Every method takes a request object, returns the full response
object, and does no interpretation whatsoever — checking
responseHeader.serviceResult and the per-operation results is your job.
| Service set | Methods |
|---|---|
| Discovery | serviceFindServers, serviceFindServersOnNetwork, serviceGetEndpoints |
| NodeManagement | serviceAddNodes, serviceDeleteNodes, serviceAddReferences, serviceDeleteReferences |
| View | serviceBrowse, serviceBrowseNext, serviceTranslateBrowsePathsToNodeIds, serviceRegisterNodes, serviceUnregisterNodes |
| Attribute | serviceRead, serviceWrite, serviceHistoryRead, serviceHistoryUpdate |
| Method | serviceCall |
A raw read with both timestamps, for example:
from o6.ns import ns0
rvi = ns0.datatypes.ReadValueId()
rvi.nodeId = o6.NodeId("ns=1;s=IntegerVariable")
rvi.attributeId = o6.AttributeId.VALUE
request = ns0.datatypes.ReadRequest()
request.nodesToRead = [rvi]
request.timestampsToReturn = ns0.datatypes.TimestampsToReturn.BOTH
response = client.serviceRead(request)
response.responseHeader.serviceResult.check()
dv = response.results[0]
print(dv.value, dv.sourceTimestamp, dv.serverTimestamp)
RegisterNodes is a service with no high-level equivalent and is worth knowing:
it asks the server for optimized NodeIds for nodes you will access repeatedly,
which can be a large win in polling loops.
request = ns0.datatypes.RegisterNodesRequest()
request.nodesToRegister = [o6.NodeId("ns=1;s=IntegerVariable")]
registered = client.serviceRegisterNodes(request).registeredNodeIds
# ... use registered[0] for reads and writes ...
release = ns0.datatypes.UnregisterNodesRequest()
release.nodesToUnregister = list(registered)
client.serviceUnregisterNodes(release)
Two service sets are deliberately absent from this list. The subscription and
monitored-item services are driven by Subscription and MonitoredItem,
which also own the Publish loop and the callback dispatch — going around them
would leave the client's bookkeeping inconsistent, so use those objects instead.
The Query service set is not exposed.
Like every other method, the serviceX calls block in synchronous code and are
awaitable in async code. The
Low-level service calls tutorial
works through more examples.
Errors and status codes¶
Three kinds of failure show up, and they are reported differently on purpose.
Bad status codes are OPC UA's normal way of saying "this operation did not
work". o6.StatusCode is an IntFlag with the full symbolic table, so it
supports bitwise tests, comparison, and int():
if o6.StatusCode.BAD in status:
...
status.check() # raises o6.StatusCodeError unless GOOD
status.check(message="reading the setpoint") # adds a note to the exception
Where a status surfaces as an exception, it is o6.StatusCodeError, which
carries code and symbol. Reads of a single target raise it; writes return
it; valueOnly=False reads put it on the DataValue.
Protocol and consistency failures — the service itself failed, or the server
returned a different number of results than requested — raise ValueError or
Exception with the offending status code in the message. These indicate a
server or configuration problem rather than a data problem.
Local usage errors raise the ordinary Python exceptions you would expect:
ValueError for a malformed range or mismatched list lengths, TypeError for a
filter of the wrong kind, KeyError for an unknown NodeId through client[...],
AttributeError for a missing or ambiguous child node, RuntimeError for
operating on a deleted subscription or item or after the loop has stopped, and
ImportError when browseInteractive() cannot find curses.
Client log output — reconnect attempts, dropped user token policies, ignored
index ranges — goes to the logger you passed to the constructor or to
client.config.logger, defaulting to the o6.client logger. Turning on
logging.DEBUG for it is the fastest way to see what the stack is doing during a
failed handshake.