Skip to content

Operations & interoperability

Historical data

Historical access needs two things, and having only the first is the usual reason a client's historyRead fails.

First, the server needs a history database, configured before start():

server.config.setHistoryDatabase(maxNodes=10)

Second, each node must be historizing and registered with the history gathering. addVariable(historizing=True) only sets the attribute and the history access levels — a client's historyRead against such a node returns BadHistoryOperationInvalid. Per-node registration currently has no public API; the internal call is:

level = server.addVariable("Level", plant, 0.0, nodeId="ns=1;s=Level", historizing=True)
server._register_historizing(o6.NodeId("ns=1;s=Level"), max_values=1000, max_response=200)

Once registered, every write is recorded and a client can read it back:

history = client.historyRead("ns=1;s=Level", start, end)
for dataValue in history.dataValues:
    print(dataValue.sourceTimestamp, dataValue.value)

Treat _register_historizing as provisional: it is private, its name and signature may change, and a public equivalent is the obvious thing to add.

Discovery and reverse connect

A server can register itself with a Local Discovery Server so that clients find it through FindServers:

server.registerDiscovery("opc.tcp://localhost:4840")
# ... serve ...
server.deregisterDiscovery("opc.tcp://localhost:4840")

semaphoreFilePath= names a semaphore file coordinating shutdown across instances. Deregistration belongs in your shutdown path.

The two LDS-side callbacks — setRegisterServerCallback, invoked when another server registers with this one, and setServerOnNetworkCallback, invoked when a server is discovered via mDNS — are part of the API but raise NotImplementedError in the current build: they depend on open62541 discovery features that are compile-time gated. Do not build a design on them without checking first.

In a reverse-connect deployment the server initiates the TCP connection to a client that is listening, which is how you reach a client behind a firewall:

handle = server.addReverseConnect("opc.tcp://localhost:4843")
# ... the server retries periodically until the client answers ...
server.removeReverseConnect(handle)

callback= receives (handle, state) on every state change. The client side is client.startReverseConnect(port), which does not return until the server has connected — see Client.

PubSub

PubSub is configured through the standard namespace-zero information model rather than through a separate Python API: you create PubSubConnection, WriterGroup, DataSetWriter, and reader components as address-space nodes and control them with the model's own methods. The o6.pubsub module adds only the runtime facilities that model cannot express:

import o6.pubsub

o6.pubsub.publish(writerGroup)                   # publish one message now
o6.pubsub.setStateMachine(component, callback)   # custom state transitions
table = o6.pubsub.offsetTable(writerGroup)       # fixed-layout byte offsets

offsetTable returns the encoded message together with typed byte offsets (OffsetType), which is what makes deterministic, pre-encoded publishing possible. setStateMachine takes a callable (current, target) -> (StatusCode, newState) and may be called from an implementation class's __init__; a component must be disabled before its state machine is replaced. PubSub requires a build and Credential with the PubSub feature scope enabled; without it these calls raise PermissionError.

Errors and status codes

The server reports failures in three ways, and the pattern differs from the client in one important respect: where a client hands you status codes to inspect, the server usually raises.

o6.StatusCodeError is raised by read when the value's status is not Good, by write, addReference, deleteReference, deleteNode, and node construction when the operation fails, and by call when a Method returns a bad status. It carries code and symbol:

try:
    server.write("ns=1;s=ReadOnly", 1.0)
except o6.StatusCodeError as error:
    print(error.symbol)      # e.g. BadUserAccessDenied

o6.StatusCode is an IntFlag, so o6.StatusCode.BAD in status works, and status.check() raises for anything but Good.

Local usage errors raise ordinary Python exceptions: ValueError for a range on a non-Value attribute, a malformed index range, or an out-of-range event severity; TypeError for a write-only Variable callback, a mismatched implement combination, an ambiguous server inference, or a @o6.call target that matches no Method; KeyError from implement when the type node has not been published yet; RuntimeError when configuration is changed after start(); and NotImplementedError for the discovery callbacks above.

Callback failures are reported to the client as status codes. Return (o6.StatusCode.BAD_..., ) from a Method or Variable callback to reject an operation. An exception escaping a callback is logged rather than propagated to the caller, so returning an explicit status is always better. Synchronous callback dispatch also rejects recursion with BadInvalidState: a Variable cannot re-enter its own read or write callback, and a Method cannot re-enter the same Method node.

Server log output — access-control decisions, rejected security policies, session activity — goes to the logger passed to the constructor or to server.config.logger, defaulting to the o6.server logger. Enabling logging.DEBUG on it is the fastest way to understand a refused connection.