Semantic Discovery¶
Picking up from Load packaged companion specs: you've been carrying NodeId strings around the distillery code — "ns=1;i=1302" for Kettle.Temperature, "ns=1;i=1204" for Status.Setpoint. Those NodeIds are correct, but they make the code brittle: if the server is rebuilt and the namespace indices shift, every NodeId literal has to be updated by hand. The last piece of the user story is to replace those literals with a semantic lookup — navigate the address space by BrowseName, with the dotted client.objects.DistillingSystem.Kettle.Temperature() style, and let o6 resolve the NodeId at the moment of the call.
This is the same operation Browse does — traverse the address space looking for a child — but exposed as a Python expression rather than a list of ReferenceDescription. The Node object you get back can be read, written, or called with (); the underlying NodeId is hidden.
This page walks through three steps:
- Navigate the distillery by BrowseName with
client.objects.DistillingSystem.Kettle.Temperature. - Read, write, and call on the resulting
Nodewith the()syntax. - Use
[]for ambiguous BrowseNames anddir(...)for REPL discovery.
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, and how to load packaged companion specs. The snippets use the distillery's DistillingSystem (ns=1;i=1000), Kettle (ns=1;i=1300), and Status (ns=1;i=1200) sub-objects as the running example.
Navigate by BrowseName¶
The distillery's address space has a stable shape: under Objects you find DistillingSystem, and under DistillingSystem you find Kettle, Status, Identification, Distillate, Actuators, Events. The Node API exposes that as a dotted path:
from o6 import Client
with Client("opc.tcp://localhost:4840") as client:
objects = client.objects # the Objects folder
distilling = objects.DistillingSystem # ns=1;i=1000
kettle = distilling.Kettle # ns=1;i=1300
temperature = kettle.Temperature # ns=1;i=1302
Names are matched case-insensitively against BrowseName — kettle, KETTLE, and Kettle all resolve to the same child. Each . triggers a server-side Browse for the named child on the current node. The result is a Node subclass (VariableNode, ObjectNode, MethodNode, …) depending on the child's NodeClass.
The Node API hides the NodeId — temperature.nodeId is the underlying integer/string, but you rarely need to touch it. Read, write, and call are all done on the Node object itself.
Putting it all together¶
from o6 import Client
with Client("opc.tcp://localhost:4840") as client:
temperature = client.objects.DistillingSystem.Kettle.Temperature
print(temperature) # ns=1;i=1302
print(type(temperature).__name__) # VariableNode
Read, write, and call on the resulting Node¶
The () operator handles every interaction. The distinction is by argument shape, not by method.
Reading a variable value becomes a call with no arguments:
Writing the variable is a call with a single positional argument:
setpoint = client.objects.DistillingSystem.Status.Setpoint
setpoint(90.0) # writes the Value attribute
Reading a non-Value attribute uses the attr= keyword:
print(temperature(attr="BrowseName")) # client1_ns1:Temperature
print(temperature(attr="NodeClass")) # VariableNode
Writing a non-Value attribute uses both value= and attr=:
Calling a method on an object is also () — pass the input arguments positionally. The parent object is picked up automatically:
distilling = client.objects.DistillingSystem
status, = distilling.Start() # StatusCode, then any output args
The distillery's Start and Shutdown take no inputs and produce no outputs, so the destructure is just status, = .... The first element of the tuple is the StatusCode (see Call a method for the full shape).
Putting it all together¶
import o6
from o6 import Client
with Client("opc.tcp://localhost:4840") as client:
# Read
temperature = client.objects.DistillingSystem.Kettle.Temperature
print("temperature:", temperature())
# Write
setpoint = client.objects.DistillingSystem.Status.Setpoint
setpoint(90.0)
# Read a non-Value attribute
print("browse name:", temperature(attr="BrowseName"))
# Call a method
distilling = client.objects.DistillingSystem
status, = distilling.Start()
print("start status:", status == o6.StatusCode.GOOD)
Disambiguate with [] and explore with dir(...)¶
Two situations don't fit the dotted path cleanly:
- Ambiguous BrowseNames. If more than one child of the same node has the same
BrowseName(case-insensitively), the dotted form can't pick a single target. Use[]with a browse path — a/-separated string of<namespace-index>:<BrowseName>segments (see Node API syntax) — and it returns all matching targets for you to pick from. The distillery doesn't have any actually-ambiguous names (both itsLevelvariables —Kettle.Levelatns=1;i=1301andDistillate.Levelatns=1;i=1401— sit under different parents, so the path/1:Kettle/1:Levelis already unambiguous), but the mechanism is the same either way — index into the returned list:
matches = client.objects.DistillingSystem["/1:Kettle/1:Level"] # a list, even with one match
kettle_level = matches[0] # pick the target
print(kettle_level())
- REPL discovery. When you don't know what children a node has,
dir(node)runs aBrowseand returns a list of the child names. Combined with<TAB>completion, it's the same workflow as the interactive browser in Browse but inside a Python REPL:
>>> dir(client.objects.DistillingSystem)
['Identification', 'Status', 'Kettle', 'Distillate', 'Actuators', 'Events', 'Start', 'Shutdown']
The list is fetched lazily on the first dir(...) and cached afterwards, so subsequent dir() calls are free.
Tip
In a Python REPL, dir(...) plus <TAB> completion is the fastest way to explore a server you don't know yet. The first dir(node) triggers a Browse; once you know the names, the dotted path does the rest.
Putting it all together¶
from o6 import Client
with Client("opc.tcp://localhost:4840") as client:
# Browse path navigation via []
matches = client.objects.DistillingSystem["/1:Kettle/1:Level"]
print(f"{len(matches)} match(es):")
for m in matches:
print(f" {m} ({m()})")
# REPL discovery
print("children of DistillingSystem:", dir(client.objects.DistillingSystem))
What's next?¶
- Node API syntax — the same syntax you just learned, viewed from the language-feature angle: the three shapes (
.,[],()) and how they map to the high-levelclient.read/client.write/client.call. - NodeIds and namespace info — when you do need the
NodeId(for logging, error messages, or handing off to a different system), this page shows how to get it back out of aNodeand how shortnames resolve to indices on the server.