Skip to content

Device Discovery Wizard

A guided flow in the gateway UI that browses an OPC UA server's address space, classifies each node onto its control plane, and emits the ISA-88-correct instance, a ControlModule (with its I/O) or a Unit, without hand-writing YAML. See ADR-0020 for the classification model.

Use this when you need to model a packaged smart device (a CMC analyzer, a Coriolis flow meter, a vendor skid PLC, a PA-DIM-conformant smart instrument) and the device's OPC UA model already carries the typing the DCS needs. The wizard is the fastest path from "endpoint URL" to "Unit applied to the cluster". Everything it does is also reachable through kubectl apply if you prefer hand-authoring.

When to use the wizard vs. each alternative

Scenario Use
Fieldbus I/O (Modbus; EtherNet/IP is an experimental, gated driver) — raw registers, no type info I/O authoring (IOModule + ControlModule)
Field device / instrument on an OPC UA server — exposes variables, no callable methods Discovery Wizard → emits a ControlModule instance (+ shared IOModule)
Module / skid exposes services (an MTP ServiceControl interface, or OPC UA Methods), DCS will orchestrate Discovery Wizard → emits Unit with serviceBinding
Skid ships a VDI/VDE/NAMUR 2658 MTP file MTP import — the same emitted resources, read out of the file with no live server needed
You already know the exact NodeIds and want full control Hand-author the CR (ADR-0001 (asset dissolution))

What the wizard emits

The wizard never emits an Asset resource. It classifies each selected node onto one of two control planes and emits the matching instance shape (ADR-0020):

  • Data plane — a ControlModule instance for a field device / instrument (a node exposing Variables, no callable Methods). The DCS owns the control loop, so the device is modelled as a control module: the CM is template-free (templateRef: ""), bound under a parent Unit you choose, and each discovered variable becomes a tag carrying the variable's node ID, dataType, and accessLevel. All the CMs from one scan share one IOModule (protocol: opcua, the endpoint's address) that the tags route through. That IOModule is controller-less, since controllerRef is optional for network protocols (ADR-0021). The wizard emits it with no Controller, and the namespace-shared network-io-probe monitors the endpoint's reachability. For a generic OPC UA device, tags are emitted without a role: discovery cannot know what a variable means (ADR 0016), so declare roles afterward if you templatize the module. A device recognized as PA-DIM is the exception. Its model declares the meaning, and the wizard reads it (see PA-DIM ingestion below).
  • Service plane — a Unit with serviceBinding for a module / skid that exposes services. The device runs its own control and the DCS orchestrates it. Two service forms exist (ADR-0045). A module recognized as an MTP PEA by its ServiceControl variable interface emits declared mtp services carrying the resolved state/command/procedure addresses. A Methods-style device emits one entry per picked OPC UA Method, whose input and output arguments are introspected from the server and mapped to IEC 61131-3 types (Bool→BOOL, Int→DINT, Double→LREAL, Float→REAL, String→STRING). Unknown types fall back to STRING and surface for you to correct.

A Unit with no services picked is rejected by the wizard (the schema requires services to be non-empty). If a candidate has neither an MTP interface nor callable Methods it is a data-plane device. Leave it as a ControlModule, or pick a different candidate.

The four-step flow

Open the wizard from the Device Discovery row in a site's Infrastructure section of the System app's sidebar, or from the Discover Devices button on the site's Infrastructure landing page. Discovery lives beside the controllers, servers, and I/O it commissions.

The Device Discovery wizard onboarding an OPC UA boiler skid, from the endpoint URL to the generated resources living in the hierarchy.

What the boiler skid actually is

The endpoint in the recording above, and in the secured one below, is Microsoft's opc-plc reference simulator (mcr.microsoft.com/iotedge/opc-plc) running as an in-cluster Pod. It is built on the OPC Foundation's .NET implementation, which shares no code with the Go stack this product's OPC UA client is written against. Endpoint discovery, the secured channel, browsing, and live method introspection are therefore shown working across an independent OPC UA implementation, and none of it runs against a server of our own.

Step 1 — Connect

Enter the OPC UA endpoint URL (opc.tcp://host:port), select a security policy and mode, and pick anonymous, username/password, or certificate auth. Discover endpoints is optional. It queries the server's discovery service and lists the security configurations it actually advertises, so you can pick the one your client trust list already supports.

Pharma deployments default-reject SecurityPolicy=None with SecurityMode=None. The Helm value gateway.opcua.allowInsecure (environment variable GATEWAY_OPCUA_ALLOW_INSECURE on the gateway) relaxes this for development. Production should always run signed-and-encrypted sessions against pinned server certificates. See Security Hardening for the certificate-pinning workflow.

The secured path runs below: endpoint discovery lists the server's advertised security profiles, the scan connects with Basic256Sha256/SignAndEncrypt and certificate auth, and the gateway's own client identity carries the secured session.

Endpoint discovery → an advertised secure profile → the scan under Basic256Sha256/SignAndEncrypt with the gateway's client identity.

The gateway discovery identity (default)

Any non-None security mode needs a client certificate at the secure-channel layer. So that Sign / SignAndEncrypt discovery works out-of-the-box under the default gateway.opcua.allowInsecure: false posture, the gateway provisions its own discovery client identity on first boot: a self-signed cert + key persisted to the dcs-opcua-client Secret in the gateway namespace, with a stable ApplicationURI (urn:cndcs:gateway:opcua-discovery). When a secured connect supplies no explicit credentials, the gateway uses this identity as the channel-layer cert automatically. Nothing to configure.

The only one-time step is on the server: it must trust the gateway's certificate. Fetch the public cert (never the private key) and add it to the server's trusted-peers directory:

curl -s "$GATEWAY/api/v1/discovery/opcua/client-identity" | jq -r .certPem \
  > dcs-discovery-client.pem

The response also carries the applicationUri, sha256Fingerprint (matching what the server logs on rejection), and validity window. Once the server trusts this cert, every DCS gateway secured discovery works without a per-endpoint Secret. The identity is long-lived and stable across restarts and upgrades. To rotate it, delete the dcs-opcua-client Secret, let the gateway re-provision on the next boot, and re-trust the new cert on your servers.

Secured endpoints — a site-specific Credentials Secret

To present a site-specific identity in place of the gateway's (or to use Certificate user-token auth, or Username/Password over a secured channel), supply an explicit Kubernetes Secret in the gateway's namespace, referenced by name. An explicit Secret always wins over the gateway identity. Choosing Sign or SignAndEncrypt (or the Certificate auth chip) surfaces the optional Credentials Secret field. Create the Secret with PEM-encoded clientCert / clientKey keys (the same key convention the Unit serviceBinding path uses):

kubectl -n dcs-system create secret generic opcua-client-cert \
  --from-file=clientCert=client.crt.pem \
  --from-file=clientKey=client.key.pem

When a Credentials Secret is set together with Username/Password auth, the username and password are read from the Secret's username / password keys, and the inline fields are ignored. Add them to the same Secret:

kubectl -n dcs-system create secret generic opcua-client-cert \
  --from-file=clientCert=client.crt.pem \
  --from-file=clientKey=client.key.pem \
  --from-literal=username=opc-browse \
  --from-literal=password='…'

The Secret name is also copied into any emitted Unit's spec.serviceBinding.security.credentialsRef. At runtime the CALL_SERVICE resolver looks a namespace-less credentialsRef up in the Unit's own namespace (site-<name>). The gateway's namespace is where the wizard created the Secret. Copy it into the site namespace after apply, or add an explicit credentialsRef.namespace to the emitted resource.

Step 2 — Scan

Pick a starting NodeId (defaults to i=85, the Objects folder) and an optional BrowseName substring filter, then Scan. Leaving the filter empty scans the whole subtree. The wizard runs a bounded recursive search and lists every Object node it finds.

Every candidate the scan finds arrives already ticked and already on the ControlModule default (a field device), so the common case needs no gesture here at all. Untick the ones you do not want to model. The checkbox reflects what carries into the next step, where you classify the candidates that expose Methods as Unit per row.

When a candidate's OPC UA type-definition resolves into a recognized device-model companion namespace, PA-DIM (http://opcfoundation.org/UA/PADIM/) or OPC UA for Devices (http://opcfoundation.org/UA/DI/), the wizard tags it with a PA-DIM device / OPC UA device chip. That is a positive classification as a data-plane field device, and it is stronger than the structural default described below (ADR-0022, #934). Candidates with no recognized type-definition fall back to the structural signal (Variables vs Methods) and the ControlModule default, which you override per row. A PA-DIM candidate additionally gets its declared signal model ingested in step 3 (see below, #935).

The service plane has no type-definition to recognize, because there is no public PEAType companion namespace. An MTP module is therefore recognized by its interface (ADR-0045, #936). When a candidate's variables load in step 3 reveals child services carrying the full VDI 2658-4 ServiceControl signature (StateCur, CommandEn, the three command channels, ProcedureCur), the wizard flips that row's default to Unit and tags it with an MTP provenance line. The flip never overrides a kind you picked by hand.

Step 3 — Review

For each selected candidate, choose the Emit as kind and edit the generated Resource name. Resource names are derived from the device display name, lowercased and sanitised to valid Kubernetes name form.

When the kind is ControlModule, a variables panel opens beneath the row. Click Browse for variables to fetch the node's Variable children. For each, the wizard reads the DataType and AccessLevel attributes (which the scan does not capture but a tag requires) and lists them read-only. Every variable shown becomes a tag on the emitted CM. A CM whose variables have not loaded, or that has none, blocks Generate. A tagless control module is exactly the empty stub this flow replaces. Deselect it or switch it to Unit.

PA-DIM ingestion

For a candidate carrying the PA-DIM device chip, the variables panel becomes a declared-signals panel: the gateway walks the device's own information model (OPC 30081), and no flat Variable scan runs. Each SignalSet member arrives named by the device's SignalTag, with its role, engineering units, range, and (for two-state signals) its true/false state labels, all read from the model:

Declared signal family Emitted tag(s) Role
Analog measurement (Temperature, Flow, Pressure, Level, analytical…) one tag state
Control signal (final-control-element readback) readback tag + <name>_SP setpoint tag feedback + setpoint
Two-state / multi-state / discrete one tag (with true/false labels where declared) state

The signal family comes from each signal's TypeDefinition resolved against the PA-DIM namespace (exact match, the same rule as the chip, ADR-0022). No tag name is ever interpreted. When the signals declare exactly one measured quantity, the emitted CM also carries spec.measuredVariable. Direct Variable children the SignalSet does not cover still ride along as plain undeclared tags. Because the emitted roles are declarations persisted on the CR, the HMI faceplate renders these devices with its role-matched widgets (setpoint/feedback bars, prominent state value) with no templatizing pass.

Device Discovery step 3 with a PA-DIM candidate's declared-signals panel: three signal rows each carrying a role chip, engineering units and range, plus the nameplate strip (Endress+Hauser · Promass F 300 · serial · firmware) and the NE 107 health strip beneath

The whole flow (the scan-time chip, the declared-signals panel, the emitted declarations, and the device health that follows the module onto its faceplate and into a declared alarm) runs below against a live PA-DIM flow transmitter:

A PA-DIM flow transmitter onboarded from its own declared model, through the faceplate's NE 107 row, to a declared device-health alarm annunciating a live maintenance call.

What the PA-DIM instrument actually is

No off-the-shelf OPC UA simulator serves PA-DIM. The instrument in the recording above is therefore a fixture in this repository, hack/docs-shots/padim-fixture. It runs on node-opcua, an independent TypeScript implementation that shares no code with the Go stack this product's OPC UA client is written against, and its address space is instantiated from the published PA-DIM companion NodeSet (1.02.0), with nothing hand-built. The type hierarchy, the DI-namespace nameplate, and each measurement variable's engineering units, range and data type therefore come from the specification file. The nameplate values on camera are representative, and they name no particular vendor's device.

pkg/opcuaclient's walk of that model is asserted against this fixture on every full CI run (make test-interop), alongside the endpoint discovery, browse, secured-channel and subscription suites driven against open62541 and opc-plc by the same target. The PA-DIM leg is thus demonstrated across an independent implementation on the same terms as the core OPC UA path above.

The MTP PEA is a fixture in this repository

The same is not yet true of MTP. No off-the-shelf simulator serves an MTP ServiceControl interface either, and the PEA in the section below is served by hack/docs-shots/mtp-fixture. That fixture is built on the same Go stack as the client reading it. Its type definitions are pinned to the published companion NodeSet, so the information model on the wire is the one VDI 2658-4 defines. The server answering the browse is still our own. Unlike PA-DIM there is no MTP companion NodeSet to load. VDI 2658-4 defines a variable shape and no OPC UA namespace, which is why the service plane recognizes a service by that shape alone (ADR-0045). A rebuilt fixture would therefore still be hand-built. What to do about that is tracked in #1463.

The panel also shows the device's IEC 61987 nameplate (manufacturer, model, serial number, firmware). It lands on the emitted ControlModule as its spec.fieldDevice asset record (type: instrument), and when the endpoint hosts exactly one PA-DIM device the shared IOModule's own record is stamped too (ADR-0043). The IOModule's copy carries the vendor and model but not the serial number: vendor and model classify the box at the endpoint, while a serial identifies one physical unit, and that identity lives on the instrument's own ControlModule so two objects cannot drift apart claiming to be the same device. After apply, the record reads back on the module's Device section and through the control-module API, and it is editable there. An instrument swap is a new serial on the module that is already in the tree.

The device's NE 107 DeviceHealth variable is declared on the ControlModule as spec.deviceHealth at emit time. After apply, the control operator polls it through the namespace io-probe (30 s cadence) and mirrors the state into status.deviceHealth. The module's HMI faceplate renders it as a DEVICE HEALTH row, and a deployment annunciates chosen states with a DeviceHealth AlarmDefinition condition (see Annunciate instrument health and ADR 0043).

Device Discovery step 4, the generated ControlModule for a PA-DIM device, scrolled to the declaration payload: role-carrying tags, measuredVariable Flow, spec.deviceHealth with the device's own DeviceHealth address, and the spec.fieldDevice instrument nameplate record

When the kind is Unit, a services panel opens beneath the row with two sections. MTP module services shows the result of the ServiceControl probe: each recognized service with the state the PEA reported at probe time and the commands its CommandEn word currently accepts, ticked by default. A ticked MTP service becomes a declared mtp entry in serviceBinding.services[], carrying the resolved control-attribute addresses. Procedure IDs and the self-completing/continuous split come from the vendor's engineering data (the MTP manifest), so you declare procedures on the Unit after apply. Methods on this Unit works as before: click Browse for Methods to fetch the candidate's Method nodes, both Methods parented directly on the device Object and Methods grouped one level below it in a component such as the OPC UA DI MethodSet (how DI-conformant smart devices normatively organise their callable surface). Tick each Method you want the DCS to call. The wizard introspects InputArguments and OutputArguments and shows the parameter list inline, and each ticked Method becomes one UnitService entry. Generate stays disabled until every Unit row has at least one MTP service or Method ticked.

Device Discovery step 3 with an MTP PEA candidate's services panel: two recognized module services, each with the state the PEA reported at probe time (Idle, Execute) and the commands its CommandEn word accepts, above a Methods section reporting that this device is service-driven through its MTP interface

The row above reached Unit by itself. A PEA carries no type-definition the scan can classify, so the candidate arrives on the ControlModule default. Loading its variables is what fires the ServiceControl probe, and recognizing the interface is what flips the kind.

The two state chips differ because the module is in two different states. A batch dosing service waits in Idle, where the transition table defines Start alone. A continuous tempering service runs in Execute, which accepts the six commands defined from there.

All emissions need a target site (resources are namespaced under site-<name>). A Unit additionally needs a process cell. A ControlModule additionally needs a parent Unit to bind under. controllerRef is optional for network protocols like OPC UA (ADR-0021), so the shared IOModule is controller-less and the wizard no longer asks for a Controller.

Step 4 — Generate

Review the JSON envelopes that will be POSTed to /api/v1/apply. They are ordered by dependency (the shared IOModule first, then the ControlModules that route through it, then any Units) and applied individually. Failures in one don't block successes in the next. Apply results render alongside each row: a checkmark for success, the error message for failure.

After Apply, the resources land in the cluster and surface in the usual places: ControlModules under their parent Unit, Units under their site's equipment view.

Device Discovery step 4, the generated Unit for an MTP PEA, scrolled to a declared mtp service entry carrying the service node address plus the resolved stateCur, commandEn, commandOp, commandExt, procedureCur and procedureExt addresses

A ticked MTP service emits the envelope above. It carries every ServiceControl address the probe resolved, and the runtime reads those declarations with no second browse. The service node and the three channels the runtime drives it through (stateCur, commandEn and commandExt) are written in the namespace-URI form. That form survives a server renumbering its namespace table (#303).

What the envelope does not carry is procedures. Procedure identity and the self-completing/continuous split are vendor engineering data from the MTP manifest, and no live server serves them.

What the wizard does not do

  • Multi-Method services on a single Unit: Each ticked Method emits one UnitService. You can pick many Methods on one Unit, but if a service needs multi-Method orchestration, edit the Unit afterwards.
  • Phase wiring: The wizard emits Units with serviceBinding (orchestrate mode) only. It does not generate PhaseTemplates or their state charts. Author phases separately once the Unit lands (see Phases).
  • Driving MTP services: CALL_SERVICE invokes Method services only and refuses an mtp service with a typed error, because an MTP service has no Method to call. Phase logic drives one by writing Table 14 command words to its ServiceControl channels, through the MTP_COMMAND / MTP_STATE / MTP_COMMAND_ENABLED builtins (ADR-0045, and Drive an MTP module service). Emitted MTP declarations carry no procedures, because a running server does not serve procedure identity or the self-completing split. Declare them on the Unit from the vendor's engineering data before orchestrating, or take them from the vendor's MTP file with MTP import. The file is the one place they are written down.
  • Templatizing a fleet: The wizard emits one ControlModule instance per device. Deriving a reusable ControlModuleTemplate from a group of identical discovered devices (then stamping instances from it) is a separate workflow. Author the template afterward and set tag roles on it.
  • Reading a generic variable's meaning: For a device without a recognized PA-DIM model, emitted tags carry an address, dataType, and accessLevel, but no role. Discovery cannot tell a setpoint from a feedback from an alarm (ADR 0016). Roleless tags render generically until you declare roles. Only a PA-DIM device's declared model changes this (see PA-DIM ingestion).
  • Server-certificate trust management: The wizard browses in trust-on-first-use mode. It accepts whatever certificate the server presents and reports the observed SHA-256 fingerprint. There is no standalone trust-list view (removed with the Asset CRD, see ADR-0001). To pin trust for the running device, set the observed fingerprint on the emitted Unit's spec.serviceBinding.security.serverCertSha256Pin after apply (see Security Hardening).

Permissions

All wizard endpoints require PermRead for browse / scan / variable- and method-introspection, and PermEngineer to call /api/v1/apply. The wizard does not invoke OPC UA Method calls itself. Introspection reads InputArguments / OutputArguments and the DataType / AccessLevel attributes, which are read-only.

Troubleshooting

Symptom Likely cause Fix
Scan returns 0 candidates Filter too narrow, or fromNode is a leaf Clear the filter, or start from i=85
Unit row stays gated even with Method ticked Method args still loading or returned an error Wait for the "N in / M out" summary; if an error appears, retry
Apply returns 400 with "services[0].name pattern mismatch" Method or argument names contained characters that survived sanitisation as empty strings Edit the resource name before Apply, or hand-edit the Unit afterwards
Apply returns 401 / 403 Operator role lacks PermEngineer Ask an admin to grant the role — see RBAC
Secured connect fails because the server rejected the client cert The OPC UA server does not trust the gateway's discovery identity Trust-list the gateway cert on the server — fetch it from GET /api/v1/discovery/opcua/client-identity, see The gateway discovery identity
Scan fails with "Client certificate material is missing" Sign/SignAndEncrypt (or Certificate auth) chosen, an explicit Credentials Secret was referenced, and it lacks clientCert / clientKey keys Recreate the Secret with both PEM keys, or drop the Secret to fall back to the gateway identity — see Secured endpoints
Scan returns 404 "credentials Secret … not found" Secret name typo, or Secret created outside the gateway's namespace Create the Secret in the gateway namespace (dcs-system by default) and re-check the name on the Connect step
Unit works in the wizard but CALL_SERVICE fails with "credentials secret not found" The emitted credentialsRef resolves in the Unit's namespace, while the Secret sits in the gateway's Copy the Secret into site-<name>, or set credentialsRef.namespace on the Unit