ADR-0001: Dissolve Asset CRD into ControlModule + Unit pattern¶
Status: Accepted
- Date: 2026-05-11
- Issue: #343
- Supersedes: the parallel-physical-layer aspect of the Smart Device Model RFC (docs/design/smart-device-model.md, #293/#294). The transport-binding ideas from that RFC survive — see Consequences.
- Followups (blocked on this ADR): #345 (Unit.spec.serviceBinding), #346 (CALL_SERVICE ST builtin), #347 (OPC UA discovery wizard), #348 (Asset CRD + AssetMethodCall deprecation).
Context¶
The physical.dcs.io/v1alpha1 model today admits three parallel ways to describe a physical-model citizen:
ControlModule— a leaf, tag-bearing element with no state machine. Tags map toIOModulechannels (Modbus/EtherNet-IP/GPIO/sim) or to FB-network outputs. Used for sensors, simple valves, motors, transmitters.Unit— a state-bearing element that hosts ISA-88 phases. Has a runtime pod, runs FB networks, ownspkg/statemachinetransitions.Asset— a smart-device wrapper introduced by the Smart Device Model RFC (#293/#294). Models OPC UA PLCs and PA-DIM smart sensors as typed properties under named transport bindings; method invocation goes through a separateAssetMethodCallPhase body type.
Three parallel layers is one too many. The Asset layer arrived to solve a real problem — IOModule is the wrong shape for an OPC UA server with hundreds of typed nodes — but it solved it by adding a peer to ControlModule and Unit rather than by widening one of them. The cost:
- Recipe authors face a forked vocabulary. A pump driven from Modbus is a ControlModule; the "same" pump driven over OPC UA becomes an Asset. Recipes branch on something that should be a deployment-time detail.
AssetMethodCallis a second Phase body type sitting next toSTSnippet/SFC. It bypasses transition guards, holds, and interlocks because method invocation lives outside the ST/SFC evaluation loop. Operators have no uniform way to express "stop the agitator if the temperature exceeds X and a remote method is in flight."- Smart skids are a poor fit for either Asset or Unit. A vendor MTP-conformant skid has its own state machine. Wrapping it in
Asset(no state machine) loses that; wrapping it inUnit(our state machine) duplicates it. We need a way to delegate to the remote state machine. - Companion specs become runtime objects. PA-DIM, ISA-95-Equipment, and similar specs are type libraries. Carrying them as values on a runtime CR conflates a design-time template with a deployed instance.
The 2026-05-11 design conversation reached a unifying observation: protocol is a driver concern, not a modeling concern. Once protocol is moved off the modeling axis, ControlModule and Unit cover the entire physical-model space without an Asset layer.
The industry precedent agrees. NAMUR NE 148 and the MTP (Module Type Package) movement frame the cell-to-package relationship as orchestrator + module with the orchestrator (the POL — Process Orchestration Layer) consuming services over OPC UA. The orchestrator does not mirror the module's internal state machine; it invokes services and observes results. That is the shape we want for smart skids.
Decision¶
- Every physical-model citizen is either a
ControlModuleor aUnit. Asset is removed as a third peer. - ControlModule: leaf, tag-bearing, no state machine. Used for sensors, transmitters, simple valves, particle counters — anything whose externally observable behavior is "tags you can read and (sometimes) write."
-
Unit: state-bearing, has ISA-88 phases, owns a runtime. Used for any equipment whose orchestration semantics include start/hold/abort/restart.
-
Protocol (Modbus, EtherNet/IP, OPC UA, GPIO, sim) is a driver concern, not a modeling concern. Either citizen can be driven by any protocol. The Smart Device Model RFC's transport-binding abstraction survives — it just attaches to the driver layer (and, for smart skids, to
Unit.spec.serviceBinding) instead of to a third CRD. -
Smart devices (vendor-packaged skids, PLCs with their own state machine, smart analyzers) are modeled as
Units with a newUnit.spec.serviceBindingfield. Orchestrate-mode only: we are always the orchestrator (POL); we do not maintain a remote-state mirror, and our Unit state machine is our view of the orchestration, not a duplicate of the device's internal state. The phase body invokes a remote OPC UA method via the newCALL_SERVICEST builtin and observes results through tags. -
Dumb devices (sensors, particle counters, simple on/off valves, transmitters) remain
ControlModules regardless of whether their wire transport is Modbus, EtherNet/IP, or OPC UA. -
No sub-units. Smart skids and their host equipment are siblings under the same Process Cell, not parent/child Units. Sub-units were never well-defined in our model and the orchestrator/orchestratee relationship is cleaner expressed via
serviceBindingthan via containment. -
Companion specs become a design-time template library, not a runtime parallel model. PA-DIM, ISA-95-Equipment, and similar specifications inform the generation of CM/Unit definitions (via the discovery wizard) but are not carried as values on running CRs. Lives under
examples/templates/and surfaces through the discovery wizard as proposed starting points. -
Discovery via live OPC UA browse drives a wizard that walks a server's address space and proposes CM/Unit definitions. The wizard is the on-ramp: pick a node, the wizard inspects type, proposes CM (if it's a leaf-typed bundle of variables) or Unit (if it's a typed object with methods and a state machine), and applies companion-spec templates when a matching type definition is recognized.
-
AssetMethodCallPhase body type is removed. Method invocation becomes aCALL_SERVICEST builtin so that calls participate in transition guards, holds, and interlocks like any other ST expression. The phase body remains ST/SFC; remote service calls are inside it, not parallel to it.
Sequence: recipe → Phase → CALL_SERVICE → remote service¶
sequenceDiagram
actor Operator
participant Recipe as MasterRecipe
participant Batch as Batch
participant Phase as Phase (ST/SFC body)
participant Runtime as Unit runtime (pkg/fbruntime)
participant Driver as OPC UA driver (pkg/driver/opcua)
participant Skid as Remote Skid (MTP module)
Operator->>Recipe: Author recipe referencing Unit with serviceBinding
Operator->>Batch: Submit Batch
Batch->>Phase: Instantiate Phase with bound Unit
Phase->>Runtime: Begin SFC execution
Runtime->>Runtime: Evaluate transition guards (ST)
Runtime->>Driver: CALL_SERVICE("StartDose", args)
Driver->>Skid: OPC UA Call(MethodNodeId, InputArgs)
Skid-->>Driver: CallResponse(StatusCode, OutputArgs)
Driver-->>Runtime: Return value + status
Runtime->>Runtime: Bind result, evaluate next transition
Runtime->>Phase: Step to next SFC step or hold on guard
Phase->>Batch: Status updates (Running → Complete / Hold / Aborted)
Notes:
CALL_SERVICEis synchronous from the ST evaluator's perspective; the driver implements timeout, retry, and cancellation semantics. A hold or abort in the middle of a long-running remote call propagates throughctx.Done(); the SFC runtime tolerates transient I/O errors and usesctx.Err()(noterrors.Is) for cancellation checks.- The remote skid's own state machine is invisible to our model. We see only the OPC UA method's status code and any output arguments. If the skid reports a fault, the Phase decides the appropriate ISA-88 response (
HOLD,ABORT) in ST. - Tags carry continuous values (temperature, level, RPM); methods carry discrete commands. A skid typically exposes both through the same OPC UA endpoint.
Alternatives Considered¶
A. Fold Asset into ControlModule ("CM as the universal leaf")¶
Widen ControlModule to admit typed properties, methods, and transport bindings; eliminate Asset by absorbing its data model.
- Pro: One fewer CRD; the transport-binding abstraction from the Smart Device Model RFC lands directly on CM.
- Con: Smart skids have state machines. ControlModule explicitly does not — that is the load-bearing distinction in our model. Making CM stateful breaks the leaf invariant that the FB runtime and HMI rely on. Once CM gains methods and state, it becomes Unit-shaped, just under a different name.
- Verdict: Rejected. The CM-vs-Unit distinction is "no state machine vs. state machine," not "simple vs. complex." Smart-skid dispatch belongs on the state-bearing side.
B. Keep Asset as a third parallel layer¶
Accept the three-way split: ControlModule for fieldbus leaves, Asset for smart devices, Unit for stateful equipment.
- Pro: Minimal disruption to the Smart Device Model RFC and existing Asset code. Each layer maps cleanly to one device pattern.
- Con: Recipe authoring forks on protocol (Modbus pump → CM, OPC UA pump → Asset).
AssetMethodCallis a parallel Phase body type that sidesteps ISA-88 hold/abort semantics. Companion specs become runtime values. The model carries the complexity of three layers when two cover the space. - Verdict: Rejected. The recipe-forking cost is the dealbreaker. Recipes must be transport-agnostic.
C. OPC UA-first model (everything is an OPC UA object)¶
Treat OPC UA's information model as the canonical schema; fieldbus protocols become "non-OPC-UA backends" that synthesize an OPC UA-shaped view.
- Pro: Aligns perfectly with PA-DIM, MTP, and the OPC UA Foundation's direction. UNS publication becomes trivial.
- Con: Imposes OPC UA's type system on every CR, including 4–20 mA loops behind a Modbus coupler where the abstraction adds pure overhead. Build cost is high and benefit accrues mostly to a future we don't ship yet. Bets the model on one vendor consortium's view of the world.
- Verdict: Rejected for the modeling axis, retained for the discovery axis. We do not force OPC UA shapes onto CM/Unit definitions, but we do use OPC UA browse as the primary discovery path for smart devices (#347) because that's where the type information lives.
D. Status quo (do nothing; ship Asset as the RFC describes)¶
Continue implementing the Smart Device Model RFC unchanged.
- Pro: Zero churn on in-flight work.
- Con: Locks in the three-layer split before any customer code depends on it.
AssetMethodCallships and becomes harder to remove later. The recipe-fork problem becomes a documentation problem rather than a model problem. - Verdict: Rejected. Now is the cheapest moment to make this change — Asset has no production users and the followup work (#345–#348) is still issue-only.
Consequences¶
Positive¶
- One vocabulary for recipes: every phase binds to a
Unit, every tag reads from aControlModule. Protocol does not appear in recipe text. - ISA-88 semantics extend to remote calls: holds, aborts, and transition guards apply uniformly because
CALL_SERVICEis an ST expression. - MTP / NE 148 alignment is structural: orchestrate-mode-only matches how the standards frame the POL/module relationship. Future MTP package import becomes a mechanical translation, not a model change.
- Companion specs stop blocking adoption: templates can ship incrementally without committing the CR schema to any one consortium's typing.
Negative¶
- In-flight Asset work is sunk cost. The Asset CRD,
AssetMethodCall, andpkg/asset.InvokeAssetMethodare deprecated in #348. The transport-binding code (OPC UA driver wiring, subscription opts, EURange handling) is salvaged — it moves to the driver layer and toserviceBinding. - Discovery wizard becomes load-bearing. Without Asset as a passive container for OPC UA browse results, the path from "I plugged in a smart device" to "I have a CM/Unit pair" depends on the wizard (#347) being good. Until it lands, smart-device onboarding is hand-authored CRs.
- Companion-spec users wait. Customers who wanted PA-DIM-driven faceplates from day one get them later, via templates, not via CR-attached spec strings.
Migration¶
No production users of Asset exist (the CRD shipped in v1alpha1 and was always documented as draft). Migration is:
- Land
Unit.spec.serviceBinding(#345). - Land
CALL_SERVICEST builtin (#346). - Land OPC UA discovery wizard (#347).
- Deprecate
Asset,AssetMethodCall,pkg/asset.InvokeAssetMethodwith a release-note callout and a one-release window before removal (#348). - Update
docs/design/smart-device-model.mdto point at this ADR and the surviving transport-binding work.
Open questions¶
- Tag publishing from Units: a Unit with
serviceBindingalso typically exposes tags (sensor reads). Whether those tags live on the Unit directly or on sibling ControlModules under the same Process Cell is left to #345. The ADR-level decision is only that protocol does not branch the model. - Discovery wizard scope: does the wizard write CRs or generate YAML for review? Left to #347. The ADR position is that wizard output must be reviewable as plain CRs before apply — GitOps-compatible.
- Companion-spec template format: TBD with #347. Likely a kustomize-style overlay applied to a generated CM/Unit base.