State Callbacks¶
Picking up from Application description: the control plane and the HMI dashboard are both connected, named, and authenticated. But the network is not always kind — the server restarts, the cable is unplugged, the OPC UA session times out. The next step is to teach your client to notice when the connection state changes and react.
o6 exposes the live state of a client's connection as a 3-tuple: (SecureChannelState, SessionState, StatusCode). The client.state property returns it on demand — there's no separate "callback registration" API, you poll it. For a dashboard that's running its own event loop, polling once per UI frame is enough; for a long-running service, a dedicated thread or asyncio task polling on a timer is the typical shape.
This page walks through state callback primitives:
- Read
client.stateand understand what each field means. - Drive an asyncio task off the state tuple to react to drops.
- Distinguish "channel renegotiated" from "session reactivated" from "totally lost".
Info
This tutorial expects the example server running in the background, and assumes you know how to create and connect a client, how to browse the address space, how to secure the connection, and how to name your client in Application description. The snippets use the distillery's DistillingSystem at ns=1;i=1000 to demonstrate the state transitions.
Read client.state¶
client.state returns a tuple[SecureChannelState, SessionState, StatusCode]. The two *State fields are enums, and the third is the most recent status code the worker thread saw. If you followed the other tutorials to this point, the channel shoud be open and the session activated:
from o6 import Client, SecureChannelState, SessionState
with Client("opc.tcp://localhost:4840") as client:
channel, session, status = client.state
print("channel :", channel) # e.g. SecureChannelState.OPEN
print("session :", session) # e.g. SessionState.ACTIVATED
print("status :", status)
The interesting values for a connected client are:
SecureChannelState.OPEN— the secure channel is up and messages can flow.SessionState.ACTIVATED— the OPC UA session is active and the server has accepted ouruser_identity_token.
client.connected is a convenience for "channel OPEN and session ACTIVATED":
The full state machine has more values, but the common ones a dashboard cares about are:
SecureChannelState |
Meaning |
|---|---|
CLOSED |
No channel. Default before connect(). |
CONNECTING |
TCP / handshake in progress. |
OPEN |
Channel up. |
CLOSING |
Channel tearing down. |
SessionState |
Meaning |
|---|---|
CLOSED |
No session. Default before connect(). |
CREATED |
CreateSession request sent / acknowledged. |
ACTIVATED |
ActivateSession accepted — fully usable. |
CLOSING |
Session tearing down. |
Anything other than OPEN and ACTIVATED means the client is not in a usable state.
Putting it all together¶
from o6 import Client
with Client("opc.tcp://localhost:4840") as client:
channel, session, status = client.state
print(f"channel={channel.name} session={session.name} status={status}")
print(f"connected: {client.connected}")
React to state changes in an asyncio task¶
For a long-running service, a single asyncio task that polls client.state is the standard shape. When the session drops, log it; when it comes back, resume work. Let's look at the state of pur control loop of the distillery — make sure the client isn't "disconnected" and send a notification the moment the state moves off ACTIVATED.
import asyncio
from o6 import Client, SessionState
async def watch_state(client):
last = None
while True:
_, session, status = client.state
if session != last:
print(f"session: {last} -> {session} (status={status})")
last = session
if session == SessionState.CLOSED:
print("connection lost — stopping work")
return
await asyncio.sleep(0.5)
with Client("opc.tcp://localhost:4840") as client:
asyncio.run(watch_state(client))
If the distillery server restarts mid-run, the session goes ACTIVATED -> CLOSING -> CLOSED. If the network drops, the channel will hit a BadCommunicationError and the status code on the state tuple will record it. The watcher only has to compare against the last value and log the transitions.
Putting it all together¶
import asyncio
from o6 import Client, SessionState
async def watch(client):
last = None
while True:
_, session, status = client.state
if session != last:
print(f"{last} -> {session} ({status})")
last = session
if session == SessionState.CLOSED:
break
await asyncio.sleep(0.5)
with Client("opc.tcp://localhost:4840") as client:
asyncio.run(watch(client))
Distinguish "channel renegotiated" from "session lost"¶
SecureChannelState and SessionState move independently, which is useful when you're debugging connection issues. A common pattern:
channelchanged butsessionis stillACTIVATED: the channel was renegotiated (cert renewal, transport refresh) and the session was kept alive across it. Nothing to do.sessiondropped butchannelis stillOPEN: the OPC UA session expired or was closed by the server. You can still try toclient.connect()again to reactivate it (the underlying channel survives).- Both
CLOSED: the connection is fully gone. Reconnect from scratch.
For the distillery, if the server is restarting while the client is running, you'll see the session field transition through CLOSING -> CLOSED while the channel field lingers on OPEN for a moment, then itself goes CLOSING -> CLOSED. Catching the session drop first is the right place to surface "reconnecting…" in the UI.
import asyncio
from o6 import Client, SecureChannelState, SessionState
async def watch_both(client):
last_ch, last_se = None, None
while True:
ch, se, status = client.state
if ch != last_ch:
print(f"channel: {last_ch} -> {ch}")
last_ch = ch
if se != last_se:
print(f"session: {last_se} -> {se} ({status})")
last_se = se
if ch == SecureChannelState.CLOSED and se == SessionState.CLOSED:
print("fully disconnected — stopping watch")
return
await asyncio.sleep(0.25)
with Client("opc.tcp://localhost:4840") as client:
asyncio.run(watch_both(client))
Putting it all together¶
import asyncio
from o6 import Client, SecureChannelState, SessionState
async def watch(client):
last_ch, last_se = None, None
while True:
ch, se, status = client.state
if ch != last_ch or se != last_se:
print(f"channel={ch.name} session={se.name} status={status}")
last_ch, last_se = ch, se
if ch == SecureChannelState.CLOSED and se == SessionState.CLOSED:
return
await asyncio.sleep(0.25)
with Client("opc.tcp://localhost:4840") as client:
asyncio.run(watch(client))
What's next?¶
- Load packaged companion specs — make
o6.ns.di,o6.ns.ia, and the other bundled companion-spec types available on a client, so the type tree in the distillery's address space can be navigated from Python.