ADR 0035: A foreign control device is an IOModule carrying a field-device record, and no new kind is minted¶
Status: Accepted Date: 2026-08-04 Issue: #1239
Context¶
A third-party PLC running its own control logic was filed as a dumb I/O card.
IOModuleSpec accepts a network protocol and an address, and
ADR 0021 made controllerRef
optional for exactly those protocols, so a Siemens S7 or a vendor gateway was
represented by a kind whose doc comment read "a physical I/O module (e.g.,
Turck, Wago, Phoenix Contact) attached to a Controller". The MTP and PA-DIM
work in #928
brings in more devices described by their own manifests, which widens the
mismatch rather than narrowing it.
ADR 0033 put the question on a footing: the asset tree gains a Field Devices branch, and a thing gets a record of its own only when no Kubernetes object is already authoritative for it. Kubernetes knows nothing about a Modbus PLC, so a record is permitted. What ADR 0033 deliberately left to this decision is whether that record is a new kind or a field on the existing one.
Three constraints from decisions already in force bound the answer.
ADR 0001 dissolved the third parallel layer once already. The Asset CRD
was removed because a peer to ControlModule and Unit forked the
recipe-facing vocabulary: the same pump became a different kind depending on
its transport, and protocol is a driver concern rather than a modeling one.
ADR 0033 allows one record per physical thing. Where a thing already has a citizen, the asset branch renders that citizen rather than minting a second object, and no record may carry a copy of a fact another record owns.
ADR 0021 preserved the IOModule reconciler semantics for this exact
population. A controller-less network module gets its Online/Fault/
Offline state from the namespace-shared probe, and that state drives the
ISA-88 Clause 7.4 auto-HOLD. ADR 0021 states the consequence plainly: a
discovered OPC UA field device that drops offline mid-batch still holds the
Unit.
Decision¶
A foreign control device is an IOModule carrying an optional
spec.fieldDevice record. No new kind is minted.
FieldDeviceRecord carries type (io-module, control-device,
instrument, gateway), vendor, model and firmware. The record says
what the box at spec.address actually is, which is the fact that was missing.
Everything about reaching it stays where it already was.
There is no schema default. An unset record means the box has not been
classified, which is a legitimate state and how every IOModule authored
before the field existed reads. Defaulting unset to io-module would restate
the kind's old doc comment as a fact about a device nobody has looked at, which
is the failure ADR 0016 refuses for undeclared
tag roles. Unclassified renders as unclassified, and the fix is declaring the
field.
The field is fieldDevice, never deviceClass. That name is already taken
in the same API group by ADR 0016, where it is a presentation grouping on
control modules whose values include controller. Two near-identical names
with unrelated meanings in one group is a permanent trap. fieldDevice also
matches the branch label ADR 0033 settled.
The kind keeps its name. Renaming IOModule would move every customer
manifest, example, tagmap entry and CLI path for a word, and the doc comment
that caused the confusion is a comment.
A vendor skid is reported rather than refused. ADR 0001 already settled
that a smart skid is a Unit with a serviceBinding, so under ADR 0033's
one-record rule that box's asset position belongs to the Unit. The IOModule
reconciler compares its address host against the serviceBinding endpoints in
its namespace and publishes status.conditions[type=DistinctAssetRecord].
False means a Unit already holds the asset position, so the module is that
skid's data plane and the Field Devices branch renders the Unit instead of a
second row.
This reports and never rejects, for two reasons. Admission cannot see other objects, so a validating rule could not express it. And two addresses on one host is a plant-wiring judgment that the product should surface rather than overrule. The comparison reads addresses, which are machine identifiers, so it does not reach into the naming-inference ban ADR 0016 imposes.
Tags are unaffected. A field device's tags enter the ISA-88 process tree as
ControlModules exactly as ADR 0020 already has discovery emit them. The box
stays in the asset tree. This composes with discovery rather than replacing it.
Alternatives Considered¶
A FieldDevice kind carrying the address and protocol. The obvious reading
of "a peer control device deserves its own kind", and it would let the type
carry device-specific fields without qualifying them. Rejected because it
replaces IOModule for that box, so tags need a second reference beside
ioModuleRef. That forks the io-probe configuration, the runtime tagmap, the
drift check, the Clause 7.4 auto-HOLD path and ADR 0020's discovery emit. It is
the third parallel layer ADR 0001 dissolved, arriving under a new name, and it
re-forks the transport-agnostic recipe vocabulary that ADR 0001 required.
A FieldDevice kind sitting beside the IOModule as a descriptive record.
Cheaper than replacement, and it keeps the tag path intact. Rejected because
one box then holds two records, which ADR 0033's one-record rule forbids, and
the descriptive record's copy of the address drifts from the operational one in
exactly the way the no-mirror rule exists to prevent.
A deviceClass discriminator, reusing the ADR 0016 word. Rejected on the
collision described above.
Renaming IOModule to something honest about the population it now covers.
Rejected on migration cost against a benefit that a corrected doc comment
delivers for free.
A zone field on the record. ADR 0031 places pods by taint and a field
device runs no pods, so nothing would consume it. Omitted until something does.
A forward list of the ControlModules a device provides. The relationship
already exists in reverse through tag.ioModuleRef, and a forward copy is a
second source of truth for one fact.
Consequences¶
- No migration. The field is optional and additive, every existing
IOModulestays valid, and an unclassified device renders as unclassified. - #928 needs no kind either. An MTP PEA is a skid, already settled as a
Unitwith aserviceBinding, and PA-DIM describes an instrument, which is an endpoint publishing tags. Both land inside this model. - #1220
has one thing to write. The Linux-native OPC UA commissioning client
populates
spec.fieldDeviceon what it commissions rather than choosing between two kinds. - The Field Devices branch renders
IOModulerows. The branch built in #1241 groups them by declared type, and it must suppress a row whoseDistinctAssetRecordcondition is false so a skid appears once. - The reconciler gains a namespace-wide
Unitread to compute that condition. It is a report rather than a gate, so a failure to compute it degrades to an absent condition rather than to a blocked reconcile. - The compliance posture is unchanged. ADR 0033 §9 holds the ISA-88
boundary and nothing here crosses it: the tags are already
ControlModules and the box was never in the physical model. - Reversibility is good in one direction. Adding fields to the record is
additive. Extracting a
FieldDevicekind later would be a migration, which is the cost this decision is choosing not to pay now and would pay then.
Related Documentation¶
- ADR index
- ADR 0001: Dissolve Asset CRD into ControlModule + Unit pattern
- ADR 0016: Declarative tag roles
- ADR 0020: OPC UA discovery device-plane classification
- ADR 0021: IOModule.controllerRef is optional for network protocols
- ADR 0033: The asset tree is a site-scoped Infrastructure branch
- I/O Modules