Skip to content

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 Server is 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.