Skip to content

fix(opcua): use config node_id as OPC-UA string NodeId#154

Merged
thiagoralves merged 1 commit into
developmentfrom
feat/opcua-string-node-id
Jul 22, 2026
Merged

fix(opcua): use config node_id as OPC-UA string NodeId#154
thiagoralves merged 1 commit into
developmentfrom
feat/opcua-string-node-id

Conversation

@thiagoralves

Copy link
Copy Markdown
Contributor

Summary

The OPC-UA server was auto-assigning numeric NodeIds (ns=2;i=N) and ignoring the node_id supplied by the Editor. That node_id traveled all the way to the runtime (serialized to conf/opcua.json, parsed into the config model) but was only used as an internal mapping/permissions key — never as the exposed OPC-UA NodeId. On top of that, auto-numeric IDs are unstable across compilations because they follow insertion order, so clients can't reliably bind to them.

This wires the Editor-provided node_id through to the actual NodeId, so clients now see stable, meaningful string identifiers like ns=2;s=PLC.Main.MotorSpeed.

Reported on the forum: Node ID in OPC UA server.

What changed

Single file: core/src/drivers/plugins/python/opcua/address_space.py.

  • Every node is created with an explicit ua.NodeId(node_id, namespace_idx) — simple variables, struct objects, struct fields (leaf and nested-object, reusing the existing parent.field path composition), and arrays.
  • The browse name is now passed as an explicit ua.QualifiedName(browse_name, namespace_idx). This is required: asyncua's arg parser defaults a bare-string browse name to namespace 0 whenever an explicit NodeId is provided, which would have silently changed browse-name behavior.
  • Runtime-side collision guard (belt-and-suspenders — the Editor already enforces uniqueness): a duplicate node_id emits a log_warn and gets a numeric suffix (_1, _2, …) to keep NodeIds unique within the namespace.
  • Empty node_id falls back to a server-assigned numeric NodeId, so a malformed config can't crash address-space build.

What did NOT change

  • No config schema changeformat_version is untouched. The node_id field already existed and was already sent by the Editor; it's simply honored now.
  • No Editor change — the field, its default (PLC.<pou>.<path>), and its uniqueness validation are already in place.
  • Internal mapping dicts (node_permissions, nodeid_to_variable) remain keyed by the config node_id, so the permission-callback resolution path is unchanged.

Compatibility note

Clients on existing deployments will see NodeIds change from ns=2;i=N to ns=2;s=<node_id> on upgrade. This is acceptable and is a net improvement: the old numeric IDs already shifted between compilations (insertion order), so nothing could safely cache them. String IDs are stable and OPC-UA-idiomatic.

Testing

  • py_compile passes.
  • Disassembled the installed asyncua's _parse_nodeid_qname to confirm exact arg-type handling.
  • Ran a real asyncua server integration test confirming: string NodeId produced correctly; namespace preserved on both NodeId and browse name; duplicate NodeId raises BadNodeIdExists (proving the dedup guard is genuinely needed); _1 dedup works; empty-config falls back to numeric.

🤖 Generated with Claude Code

The OPC-UA server auto-assigned numeric NodeIds (ns=2;i=N) and ignored
the node_id supplied by the Editor, which was only used as an internal
mapping/permissions key. Auto-numeric IDs are also unstable across
compilations since they follow insertion order.

Create every node with an explicit ua.NodeId(node_id, ns) so clients see
stable, meaningful string identifiers (ns=2;s=PLC.Main.MotorSpeed). The
browse name is now passed as an explicit ua.QualifiedName in the same
namespace, since asyncua would otherwise default a bare-string browse
name to namespace 0 when an explicit NodeId is given.

Applies to simple variables, struct objects, struct fields (leaf and
nested-object), and arrays.

Runtime-side collision guard (belt-and-suspenders; the Editor already
enforces uniqueness): duplicate node_id values emit a warning and get a
numeric suffix (_1, _2, ...) to keep NodeIds unique within the
namespace. An empty node_id falls back to a server-assigned numeric
NodeId so a malformed config cannot crash address-space build.

No config schema change (format_version unchanged) and no Editor change:
the node_id field was already serialized and parsed; it is now honored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thiagoralves
thiagoralves merged commit d980990 into development Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant