Skip to content

ADR 0020: OPC UA discovery classifies each node onto a control plane — field devices become ControlModule instances, modules become Units

Status: Accepted Date: 2026-07-12 Issue: #922

Context

The Device Discovery wizard (#347) browses a live OPC UA address space and turns discovered nodes into CRs. For a leaf-shaped device it emits a ControlModuleTemplate — and the emitted template is an empty stub: moduleType: discovered, network: {blocks: []}, tags: []. The wizard holds the device's OPC UA variable node IDs during browse and discards every one at emission (internal/gateway/static/js/views/discover.js, buildCRFromCandidate). The operator then runs the #839 instantiate flow and rebuilds, from scratch, the addressing the wizard already had.

This is a modeling error, not merely a missing feature. A discovered physical device at a live endpoint is instance-shaped: it is one specific thing at one specific address. A template is by definition the address-free reusable class — precisely the artifact that throws away what discovery produced. #347 shipped template-emission for leaf devices from its first commit; the classifier that would have typed the device (companion-spec recognition — PA-DIM, OPC 40xxx) was deleted with the Asset dissolution (ADR 0001, #348) and its dead surface swept in #857, leaving "every candidate defaults to ControlModuleTemplate; the user classifies Units manually."

The founder's #841 clip-review question — "Wasn't there a control module added? … It should be a control module instance, no?" — is the correct instinct. But "emit an instance instead of a template" alone underdescribes the fix, because the right shape depends on what kind of device was discovered. This ADR was written after verifying that intuition against the process-automation standards that govern the ISA-88 ↔ OPC UA boundary.

What the standards say (verified 2026-07-12 against primary sources). The industry partitions the OPC UA integration boundary into two non-overlapping models:

  • Field devices / instruments are modeled by device companion specifications — PA-DIM (Process Automation Device Information Model, OPC UA companion spec OPC 30081; a nine-body consortium incl. NAMUR, FieldComm Group, OPC Foundation, ZVEI) and the base OPC UA for Devices (DI, OPC 10000-100). They expose variables, identification (IEC 61987), and health (NAMUR NE 107 — Failure / Function Check / Out-of-Specification / Maintenance — carried as OPC UA DI DeviceHealth alarm types). A field device has no autonomous control of its own; the supervising system closes the loop.
  • Modules / skids are modeled by MTP (Module Type Package, VDI/VDE/NAMUR 2658, internationalizing as IEC 63280). An MTP describes a Process Equipment Assembly (PEA) — an autonomous modular unit — integrated by a Process Orchestration Layer (POL). MTP runtime communication is OPC UA (PEA = server, POL = client; binding defined in 2658 Blatt 5.1, not a separate OPC UA companion spec). Critically, an MTP service is an ISA-88 construct: the 2024 joint NAMUR/ISA-88 working group states MTP services "are essentially the Equipment Procedural Elements of ISA 88" with a "very similar state model" — the two are being actively aligned, though not yet unified.

That partition maps cleanly onto the ISA-88 physical model this product already implements. A field device is a Control Module — the lowest commandable equipment grouping — whose I/O the supervising DCS reads and whose loop the DCS owns. A PEA/skid is a Unit with its own control: an opaque box exposing services, orchestrated rather than decomposed. Our Unit.serviceBinding (endpoint + security + credentialsRef + method-derived services) is already exactly an MTP-adjacent, service-plane integration — it needs no Controller and no IOModule because the PEA is its own controller and I/O. The leaf/data path had no equivalent: field-device variables had no destination, so they were dropped.

ISA-88 clause grounding (verified 2026-07-13 against ANSI/ISA-88.00.01-2010 Clause 3). The physical-model mapping above rests on three normative definitions, checked directly against the Part 1 text (the ISA-88 Bundle/ PDFs, absent when this ADR was first drafted, were restored before this verification):

  • Control Module"the lowest level grouping of equipment in the physical model that can carry out basic control" (§3.1.13). NOTE 3 is decisive for the data-plane mapping: "The control module level contains the interfaces to the physical equipment." A discovered field device is an interface to physical equipment; ISA-88 places that interface at the Control Module level, so emitting a CM instance carrying the device's variable node IDs is the standard-correct shape, not merely a convenient one. (This ADR earlier paraphrased §3.1.13 as "lowest commandable equipment grouping"; the normative wording is "lowest level grouping … that can carry out basic control" — same intent, quoted here for precision.)
  • Basic control"the control dedicated to establishing and maintaining a specific state or behavior of equipment and process", including "regulatory control, interlocking, monitoring, exception handling, and discrete or sequential control" (§3.1.5). This is what "the supervising DCS closes the loop" means concretely: the field device has no autonomous control; the DCS's runtime FB network supplies the CM's basic control over the discovered tags.
  • Unit"a collection of associated equipment modules and/or control modules that can carry out one or more major processing activities" (§3.1.71). This grounds both halves of the decision: a Unit contains Control Modules, so the discovered CM instance's parentKind: Unit is the standard containment relation (§3.1.13 NOTE 2 additionally forbids omitting the CM level); and a service-exposing PEA/skid that "carries out a major processing activity" is itself Unit-shaped, which is why the service-plane path emits a Unit + serviceBinding.

Nothing in Clause 3 contradicts the framing; the verification tightened the §3.1.13 wording (above) and surfaced NOTE 3 as the load-bearing citation for the CM-instance decision. The ISA-88 ↔ MTP-service linkage (§7.5 procedural state model alignment) remains corroborated by the 2024 joint NAMUR/ISA-88 WG paper rather than settled normative text, per the caveat in the previous paragraph.

Two caveats bound the framing and are honored below. First, the "field device vs module" split is a real standards partition but the crisp "data-plane / service-plane" phrasing is our internal design vocabulary, not literal NAMUR terminology, and the boundary blurs — a PEA internally contains field devices — so classification must be per node, not per endpoint. Second, MTP's exact service state/command enumeration is not settled (alignment in progress); we therefore do not hardcode MTP state names and do not claim MTP conformance for serviceBinding.

A third tension surfaced during design: an opcua IOModule requires a controllerRef, and a Controller in this codebase is a compute node that joins the cluster (ADR 0004), not a source of tags. A discovered remote PLC is neither — it is network I/O read by an existing runtime node. controllerRef is read nowhere on the tag-read path (internal/adapter/, pkg/driver/opcua/, cmd/unit-runtime/); it only drives the IOModule's ControllerFound status condition and io-probe scheduling.

Decision

Device Discovery classifies each discovered OPC UA node onto one of two planes and emits the ISA-88-correct instance shape for that plane. Leaf devices become ControlModule instances, never templates.

  • Data-plane (field device / instrument — a node exposing Variables, no autonomous services): emit a template-free ControlModule instance (templateRef: "") under an operator-chosen parent Unit (parentKind: Unit), its tags[] carrying the discovered variable node IDs with dataType and accessLevel (read from the node attributes), plus a single shared opcua IOModule for the endpoint. Each tag routes through the IOModule (ioModuleRef + address). The IOModule's controllerRef names an existing Controller in the target site (the runtime node that reaches the endpoint) — the wizard auto-selects it when the site has exactly one and prompts otherwise. No Controller is fabricated.
  • Service-plane (module / skid — a node exposing Methods / a service state machine): emit a Unit + serviceBinding, as the wizard already does. No IOModule, no Controller.
  • Classification is per node. The signal today is structural — Methods present ⇒ service-plane; Variables only ⇒ data-plane. A PA-DIM (PADIMType) or MTP/PEA type-definition, when present, is a strong positive signal for the respective plane. The former "Emit as" dropdown becomes a manual override of the classification, not the primary decision. ControlModuleTemplate is removed as a leaf emit kind.

A template-free ControlModule is a first-class, already-supported shape: the CRD requires the templateRef key but sets no minLength; the control-plane compiler no-ops on an empty templateRef; and the physical reconciler has a dedicated "direct tags with raw I/O addresses (non-template CMs)" path. The runtime reads such a tag straight from its ioModuleRef + address with no ControlProgram.

We explicitly do not claim MTP or PA-DIM conformance: serviceBinding consumes raw OPC UA Methods, not an MTP service state machine, and the data-plane path reads plain Variables, not the PA-DIM information model. Native MTP/PA-DIM support (type-definition-driven classification, the aligned service state model, NE 107 health) is a separate future effort (#922 follow-up B), not this decision.

Alternatives Considered

  • Emit an instance and a minimal template (template captures the interface, instance references it): rejected — a template-free CM is fully supported, so the template is dead weight; and a template minted from a single discovered exemplar is not a reusable class, just ceremony. Deriving a genuine reusable template from a fleet of identical discovered devices is a real workflow, but a distinct one, and not what a single-device discovery should force.
  • A Unit-level data binding, symmetric with serviceBinding (a dataBinding/tagSources[] on the Unit carrying field-device variables): rejected as a category error. The standards place field devices at the instrument level (Control Module), not the module level (Unit). IOModule is already the correct I/O abstraction for a field device's data; putting that binding on the Unit would conflate an instrument with a module.
  • Auto-emit a Controller for the discovered endpoint (fully self-contained 3-object emit): rejected. A Controller is a compute node that joins the cluster; fabricating one for a remote third-party PLC invents a node that never joins (permanent AwaitingNode), or forces Simulation: true, a lie about a real device. The correct owner already exists — the runtime node reading the endpoint.
  • Classify per endpoint, one shape per device: rejected — a single OPC UA server can expose both service objects and device/variable objects (a PEA contains field devices), so the unit of classification is the node.
  • Keep the manual "Emit as" dropdown as the primary decision (status quo): rejected — it makes correct modeling the operator's burden and reproduces the #348-deleted classifier's absence as a UX cost.

Consequences

  • #922 (this issue) ships the data-plane half with no CRD change. New gateway backend endpoint POST /api/v1/discovery/opcua/variables reads per-variable DataType and AccessLevel (neither is captured by the current scan, and both are required on Tag). discover.js buildCRFromCandidate is rewritten to emit [IOModule, ControlModule] in dependency order; the leaf ControlModuleTemplate branch is deleted; step 3 gains parent-Unit and Controller selection for CM candidates. The service-plane (Unit + serviceBinding) path is unchanged.
  • Follow-up A (#927): make IOModule.controllerRef optional for network protocols (opcua, ethernetip, modbus-TCP) — a network-reached device owns no compute node. Retires the "reference an existing Controller" requirement at the root and lets controller-less sites work. Requires an io-probe scheduling story for controller-less IOModules.
  • Follow-up B (#928): MTP- and PA-DIM-native discovery — classify on PEA/PADIMType type-definitions, map the aligned ISA-88/MTP service state model, and carry NE 107 device health. This is where genuine standards conformance lives; the exact MTP state/command set must be taken from VDI/VDE/NAMUR 2658-4:2022 directly when it is built.
  • The classifier is reintroduced, but scoped. Only the structural data/service split and (when present) type-definition signals — not the full companion-spec registry #857 deleted. OPC 40080, cited in the old registry, was not verifiable as a real companion spec and must not be reintroduced without checking the OPC Foundation registry.
  • Compliance traceability landed (2026-07-13). The ISA-88 Bundle/ PDFs were restored and the physical-model mapping verified against Clause 3 (see "ISA-88 clause grounding" above); docs/compliance/isa88.md now carries the discovered-device Control Module / Unit mapping in its Physical Model section. The ISA-88 ↔ MTP-service linkage this ADR relies on remains corroborated by the 2024 joint NAMUR/ISA-88 WG paper, independent of the PDFs.
  • Behavior change: discovering a leaf device no longer produces an Equipment Library template entry; it produces a bound CM instance under a Unit. Docs (docs/device-discovery.md) and the #841 clip are re-scoped accordingly.
  • Reversibility: high for #922 (client-side emission + one additive backend endpoint, no schema change). Follow-ups A and B touch CRD/API surface and would carry their own ADRs.