Skip to content

Browsing

Demonstrates how to walk an OPC UA server's address space from the client: starting from the standard Objects folder, navigating to a specific node by browse name (dot syntax) or browse path (index syntax), enumerating the references coming out of a node with client.browse(...), and reading / writing the value of a leaf variable through the resulting Node handle.

The example is wired against S03_firststeps.py. The address space we walk is:

Objects/
└── Plant/                  (Object)
    ├── Temperature         (Double,  ns=1;i=1001)
    ├── Pressure            (Double,  ns=1;i=1002)
    ├── Status              (String,  ns=1;i=1003)
    ├── Counter             (Int32,   ns=1;i=1004)
    ├── Running             (Boolean, ns=1;i=1005)
    └── Add                 (Method,  ns=1;i=2001)

The example shows both the synchronous style (blocking with Client(...) and direct attribute / value access) and the asynchronous style (async with, await everywhere) so you can compare the two.

This example needs a running server

Start Server First Steps first:

python examples/server/S03_firststeps.py

Browsing the address space is one of the most useful patterns in a real OPC UA client: server-assigned NodeIds are often not known by the client, and the browse name of a node is the stable identifier you actually want to program against. The o6 SDK gives you three layers of convenience on top of the raw Browse service: client.browse(...) returns a list of references, client[NodeId] hands back a Node, and Node itself supports dot syntax (.Plant) and browse-path syntax (["/Objects/Plant"]) so you can walk the tree without manually tracking NodeIds.

1. Connection Setup

Same as in C02_firststeps.py: build the endpoint URL from the local hostname so it matches the URL the server advertises during discovery.

print("--- 1. Connection Setup ---")
localhost = "localhost"
endpoint_url = EXAMPLE_ENDPOINT
print(f"Connecting to {endpoint_url} ...")

2. Sync: Get a Node Directly

client[nodeid] returns a Node object that mirrors the address space entry. In a synchronous with block the call blocks until the NodeClass and BrowseName attributes have been read; from there the Node is a local handle that can be printed, browsed, and indexed.

with Client(endpoint_url) as client:
    print()
    print("--- 2. Sync: Get a Node Directly ---")
    node = client["i=85"]  # The standard `Objects` folder
    print(f"Node       = {node}")
    print(f"BrowseName = {node(attr=AttributeId.BROWSE_NAME)}")
    print(f"NodeClass  = {node(attr=AttributeId.NODE_CLASS)}")

3. Sync: Dot Traversal

node.Child walks one level into the address space by browse name (the same way you'd navigate Python objects).

We use this to reach Plant and then Plant.Temperature. Both Plant (its NodeId is allocated at server startup) and Temperature (its data type is Double) live in the user namespace.

print()
print("--- 3. Sync: Dot Traversal ---")
node = node.Plant
print(f"Node       = {node}")
print(f"BrowseName = {node(attr=AttributeId.BROWSE_NAME)}")
print(f"NodeClass  = {node(attr=AttributeId.NODE_CLASS)}")

4. Sync: Enumerate Children with client.browse(...)

client.browse(node, ...) walks one level out from node and returns a list of ReferenceDescriptions. The result_mask keyword controls which fields each reference carries back: 63 asks for every reference field (browse name, display name, node class, type definition, …) so we can print the children with full detail.

print()
print("--- 4. Sync: Enumerate Children with client.browse(...) ---")
refs = client.browse(
    node,
    resultMask=(
        ns0.datatypes.BrowseResultMask.BROWSE_NAME
        | ns0.datatypes.BrowseResultMask.NODE_CLASS
        | ns0.datatypes.BrowseResultMask.DISPLAY_NAME
        | ns0.datatypes.BrowseResultMask.TYPE_DEFINITION
    ),
)
for r in refs:
    print(
        f"  {r.nodeId}  "
        f"browseName={r.browseName}  "
        f"display_name={r.displayName}  "
        f"type={r.nodeClass.name}"
    )

5. Sync: Read and Write Through the Node

Calling a Node with no arguments (node()) returns the current value of its Value attribute; calling it with a value (node(value)) writes that value. The SDK interprets the OPC UA data type from the Python type of the value, so a Python float is written as a Double, a Python int as an Int32, and so on.

print()
print("--- 5. Sync: Read and Write Through the Node ---")
node = node.Temperature  # Drill into a leaf variable
print(f"Node  = {node}")
print(f"Value = {node()}")

node(25.0)  # Plain Python float -> OPC UA Double
print("Wrote Value -> 25.0")
print(f"Value = {node()}")

6. Async: The Same, Inside an Event Loop

The asynchronous form is useful when the surrounding application (a web server, a UI loop, ...) is already running its own asyncio event loop. Inside an async with Client(...) as client: the same client[...] and client.browse(...) calls become awaitable. The dot-traversal also returns an AwaitableNode that chains awaits for you.

async def main() -> None:
    async with Client(endpoint_url) as client:
        print()
        print("--- 6. Async: The Same, Inside an Event Loop ---")
        # client["..."] returns an awaitable; the `await` resolves it
        # to a Node handle.
        node = await client["i=85"]
        node = await node.Plant  # async dot-traversal
        node = await node.Temperature  # async dot-traversal into a leaf

        print(f"Node  = {node}")
        print(f"Value = {await node()}")

        await node(30.0)
        print("Wrote Value -> 30.0")
        print(f"Value = {await node()}")


asyncio.run(main())
print()
print("=== Example completed ===")

Source Code

Download C03_browsing.py to run it locally.