Skip to content

ADR 0045: An MTP service is recognized by its ServiceControl variable shape and modeled with the VDI 2658-4 vocabulary; ISA-88 presentation is a declared correspondence, and no MTP conformance is claimed

Status: Accepted Date: 2026-08-10 Issue: #936

Context

ADR 0020 gave discovered OPC UA devices the ISA-88-correct shape — field device → ControlModule, module/skid → Unit with a serviceBinding — and ADR 0022 made data-plane classification native by keying on the PA-DIM / OPC UA DI type-definition namespaces. The service plane stayed on two proxies: classification by "exposes Methods", and a serviceBinding whose services are raw OPC UA Method invocations with no states, commands, or procedures. #928 deferred going native to #936 because the normative source, VDI/VDE/NAMUR 2658 Blatt 4:2022, is a paid standard; it was purchased on 2026-08-09 (cndcs-finance#38) and is archived in the CNDCS Drive MTP Bundle folder, with gitignored derivative notes in docs/mtp/vdi2658-4-service-model.md.

Blatt 4 defines the module service model: a PEA (Process Equipment Assembly) exposes services a POL (Process Orchestration Layer) orchestrates; each service implements a mandatory, non-configurable state machine of 16 states in five levels, driven by ten commands with a mandatory bitwise coding (Table 14); each service has one or more procedures, self-completing or continuous; and each service exposes the ServiceControl interface (Table 13) — the state feedback pair (StateCur, CommandEn), three command channels (CommandOp/CommandInt/CommandExt), procedure channels, and the operation-mode machinery.

Three facts shape the design space:

  1. There is no type-definition to key on. MTP's OPC UA runtime binding is Blatt 5.1, which maps ServiceControl attributes onto nodes the MTP manifest file names — it is not a companion specification with a public PEAType NodeID the way PA-DIM has PADIMType. (Blatt 5.1 is also currently a draft edition at DIN Media and was deliberately not purchased.) The ADR 0022 mechanism therefore cannot extend to the service plane.
  2. The state machine is similar to ISA-88, not identical. The standard itself says services follow a state machine "similar to the concept of ISA 88 or DIN EN 61512-1" (Section 4). Our pkg/statemachine implements the ISA-88 13-state/8-command model; MTP has 16 states and 10 commands, two of which (Restart, Complete) have no ISA-88 counterpart at all. The joint NAMUR/ISA-88 working group is aligning the models but they are not unified (ADR 0020).
  3. A from-memory enumeration was already refuted once. #928 records that a candidate state/command set written from memory did not survive contact with the standard. The enumeration must be transcribed, and pinned by tests that a reviewer can check against the PDF.

Decision

Recognition: the ServiceControl variable shape is the service-plane signal. A discovered node is recognized as an MTP service when its child variables carry the full ServiceControl core signature — all of StateCur, CommandEn, CommandOp, CommandInt, CommandExt, ProcedureCur (mtpservice.IsServiceControlShape). All six are required: the interface is not optional in the standard, a partial match is far more likely a BrowseName collision than a partial MTP implementation, and the cost of a false positive is emitting a Unit for a field device. A node whose children include such service nodes is a PEA and pre-classifies Unit. This is still structural in the letter — we match names, not a type — but it is the standard's own mandatory interface rather than the "exposes Methods" proxy, and it is exactly the variable set the model consumes afterwards. The Methods proxy remains as operator guidance for unrecognized nodes; recognition only moves the default, the operator still decides.

Vocabulary: pkg/mtpservice carries the standard, verbatim and pinned. The 16 states with their five levels, the ten commands, the Table 14 bit codings (states, commands, CommandEn), the Figure 3 transition table with level preemption, and the procedure self-completing/continuous split live in one package with no other dependencies than pkg/s88. Unit tests pin every constant to the integer values printed in Table 14 and every transition to Figure 3, so a future edit is visibly an edit of a normative constant. The decoders are strict: a state or command word that does not carry exactly one defined bit decodes to nothing rather than a guess, because the standard requires exactly one bit set and the PEA itself zeroes malformed control words.

ISA-88 presentation: a declared correspondence in one place. mtpservice.ToS88State / ToS88Command map the MTP vocabulary onto pkg/s88 for every surface that presents an MTP service as an ISA-88 Equipment Procedural Element. The non-obvious rows are deliberate: MTP Unhold/Unholding correspond to ISA-88 Restart/Restarting (both are the Held → transient → running exit), while MTP's own Restart command (Execute → Starting, re-running the procedure) and Complete (external completion of a continuous procedure) have no ISA-88 counterpart and map to nothing. Surfaces consume the correspondence; nobody re-derives it locally.

pkg/statemachine is not refactored. The ISA-88 engine keeps its hardcoded 13/8 table; pkg/mtpservice is a parallel vocabulary package, not a second tenant of a parameterized engine. We are the POL: we track the state a PEA reports (StateCur is feedback, the PEA owns the machine), we do not run an MTP state machine of our own, so the engine mechanics — history, callbacks, mutexes — have no MTP consumer today. A future MTP simulator can revisit this.

No conformance claim, stated where it binds. Nothing in this line of work claims MTP conformance: the model is transcribed from Blatt 4, but the runtime binding (Blatt 5.1) is unpurchased, the ISA-88/MTP alignment is not unified, and no AML manifest import/export exists. docs/standards/mtp.md carries the posture; the package doc and this ADR repeat it so a future surface cannot inherit an implied claim.

Alternatives Considered

Keep the Methods proxy and only add the state vocabulary. Rejected: #936's first scope item exists because the proxy misclassifies — a PA-DIM instrument with one configuration Method reads as service-plane, and a PEA whose services are variable-driven (the normative interface is variables, not Methods) reads as data-plane.

Parameterize pkg/statemachine for both models. Rejected for blast radius: every consumer of New()/NewWithState() (batch, physical, procedural controllers) would recompile against a changed constructor for a capability nothing needs — we do not run the PEA's machine, we read its feedback.

Map all ten MTP commands onto ISA-88 by stretching the nearest neighbors (Restart → s88.Restart, Complete → SC). Rejected: MTP Restart and ISA-88 Restart are different transitions from different states with different meanings, and a correspondence that lies about two rows poisons trust in the other fourteen. Unmapped is honest and the compiler makes callers handle it.

Consequences

  • pkg/opcuaclient grows a PlaneSignalService alongside PlaneSignalData (ADR 0022) and a ServiceControl-shape walk; the discovery wizard flips its default kind to Unit on the service signal — the flip ADR 0022 explicitly deferred here.
  • UnitServiceBinding can grow a declared MTP service form (per-service ServiceControl node addresses and declared procedures) so an emitted PEA service carries states rather than a bare Method NodeID. The declare-at-emit pattern follows ADR 0043: discovery resolves and persists addresses; runtime consumes declarations, never re-guesses.
  • The two unmapped commands (Restart, Complete) mean any surface that renders MTP commands through the ISA-88 command set will show eight of ten; a native MTP surface must consume the MTP vocabulary directly. The Unit detail's service panel (GET /units/{name}/mtp-services, #1405) is the first such surface and does exactly that: it renders the MTP state names and the CommandEn accepts-list verbatim, and translates nothing into ISA-88.
  • Because a PEA's state is feedback and is never mirrored into a CRD status, every surface that shows it has to read it. That makes an OPC UA session a thing a read path needs and cannot open per request, so the gateway pools sessions the way the phase run does (#1379, #1405) — keyed on the resolved principal, so two Units reaching one PEA as different users never share one.
  • Purchasing Blatt 5.1 when it leaves draft status would replace the BrowseName-shape recognition with the manifest-declared binding; the recognition function is the single point that would change.
  • Recognizing a service by variable shape rather than by namespace also means there is no MTP companion NodeSet, and so no third-party address space to instantiate a fixture from. That settles the question #1463 asked: the PA-DIM rebuild (#1454) was a strict improvement because the specification's own file replaced approximated nodes, and MTP has no equivalent — any MTP fixture is hand-built on any stack. What a foreign server can still disagree with us about is how it types a node, and one did. WriteServiceWord inferred the variant to encode from the node's sampled value; opc-plc serves Int64 for a node it declares UInt32 whenever its address-space file gave that node an initial value, so the inference produced a write the server refused where the standard's plain UInt32 would have landed. The variant now comes from the DataType attribute, which is the attribute a server validates a write against, and the sampled value is the fallback for a server that does not answer it or declares an enumeration. make test-mtp-interop holds that against opc-plc.