Server¶
This chapter is the complete story of o6.Server. This page covers the two
routes to an address space and how one API serves both synchronous and
asynchronous code.
Contents¶
| # | Section | What it covers |
|---|---|---|
| 1 | Lifecycle & configuration | Creating and running a server, and configuring it. |
| 2 | Building the address space | The imperative route: nodes, reads and writes, methods, references, and browsing. |
| 3 | Declared types | The model-first route. |
| 4 | Implementing behaviour | Attaching Python behaviour to a declared type. |
| 5 | Server callbacks | The exact callback signatures and their resolution order. |
| 6 | Events, monitored items & timers | Server-side notification and scheduling. |
| 7 | Access control & roles | Authentication and role-based access control. |
| 8 | Notification handler | Subclass-free, decorator-friendly registration of server-side application notifications (lifecycle, secure channel, session, service, subscription, monitored item, discovery, audit). |
| 9 | Operations & interoperability | History, discovery, reverse connect, PubSub, and status codes. |
Its sibling chapter, Client, covers the other half of the protocol.
Two ways to build an address space¶
Everything a server exposes is nodes, and o6 gives you two routes to them.
The imperative route is a handful of addX helpers on the server object:
addObject, addVariable, addMethod, and friends. You call them at runtime,
each one creates exactly one node, and you get a Python node handle back. This
is the shortest path from nothing to a working server, and it is the right
choice for scripts, gateways, and address spaces whose shape is decided by
configuration rather than by a model.
The declarative route is an OPC UA information model written as Python
classes — @o6.objecttype, @o6.variabletype, @o6.datatype, and the rest —
collected in a namespace module that the server publishes with
server.ns.append(module). The types then behave like classes: instantiating
one creates a complete typed subtree, and the Python class carries the
behaviour. This is the right choice whenever the address space is a model —
a companion specification, a product line, anything a client should be able to
discover by type.
The two mix freely: a declarative model can sit next to imperatively created nodes in the same server, and both end up in the same nodestore.
One API, synchronous or asynchronous¶
The server owns an asyncio event loop, which drives all I/O, timers, and callbacks. How that loop is driven depends on how you created the server.
When no loop is passed and none is running, the server creates its own loop and,
on start(), runs it on a background daemon thread. Synchronous calls —
server.read(...), server.write(...), server.addVariable(...) — are
dispatched onto that thread and block until they complete. This is the plain
script case, and it needs no asyncio knowledge at all.
When a loop is already running (or one is passed as loop=), the server uses
it and spawns no thread. Async tasks always await dual-mode operations,
including node creation and immediate local reads:
import asyncio
import o6
async def main():
server = o6.Server(port=4840)
variable = await server.addVariable("Async", server.objects, 1.0)
async with server:
print(await variable())
asyncio.run(main())
The same operation names work in sync and async code; async tasks add await.
This also applies before startup and from a different loop. See
creation in synchronous and asynchronous code.
Synchronous SDK callbacks retain immediate local results; pending same-loop
work returns an awaitable without blocking. Use an async callback when its
continuation needs that result.
Two consequences are worth internalizing:
- Callbacks — Method calls, Variable read/write callbacks, monitored-item notifications, timers — all run on the server's loop thread. Blocking inside one blocks the whole server. Hand slow work to a queue or a thread pool.
- A
Serveris bound to one loop for its lifetime. Use one server per loop.
Where to go next¶
- Writing a Nodeset in Python — every decorator, in depth, with a complete reference model.
- Implementing Object Behavior — the declare/implement/register cycle and its precedence rules.
- Server callbacks — exact signatures and resolution order for Method, Variable, ObjectType, and VariableType callbacks.
- Using Nodesets — packaged companion specifications and XML nodesets.
- Node API — the object syntax used for every node handle here.
- Client — the other half, for talking to the server you just built.
- Performance — batching, buffer sizes, and large arrays.
- API Reference — generated signature-level reference.