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: Unitis 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 aUnit+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
ControlModuleinstance (templateRef: "") under an operator-chosen parent Unit (parentKind: Unit), itstags[]carrying the discovered variable node IDs withdataTypeandaccessLevel(read from the node attributes), plus a single sharedopcuaIOModulefor the endpoint. Each tag routes through the IOModule (ioModuleRef+address). TheIOModule'scontrollerRefnames 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. NoControlleris fabricated. - Service-plane (module / skid — a node exposing Methods / a service
state machine): emit a
Unit+serviceBinding, as the wizard already does. NoIOModule, noController. - 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.ControlModuleTemplateis 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(adataBinding/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).IOModuleis 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
Controllerfor the discovered endpoint (fully self-contained 3-object emit): rejected. AControlleris a compute node that joins the cluster; fabricating one for a remote third-party PLC invents a node that never joins (permanentAwaitingNode), or forcesSimulation: 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/variablesreads per-variableDataTypeandAccessLevel(neither is captured by the current scan, and both are required onTag).discover.jsbuildCRFromCandidateis rewritten to emit[IOModule, ControlModule]in dependency order; the leafControlModuleTemplatebranch 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.controllerRefoptional 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/
PADIMTypetype-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.mdnow 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.
Related Documentation¶
- ADR 0001: Dissolve Asset CRD into ControlModule + Unit pattern — deleted the companion-spec classifier this ADR partially rebuilds
- ADR 0004: Node join is a deployment-layer concern — why a Controller is a compute node, not a tag source
- docs/device-discovery.md — the wizard this ADR re-scopes