Files
lmxopcua/docs/research/drivers/mtconnect-agent.md
T
Joseph Doherty 8fc147d8d4
v2-ci / build (push) Successful in 3m14s
v2-ci / unit-tests (push) Failing after 9m13s
docs: driver-expansion program — 8 research reports + 7 design docs, parallel-reviewed
Adds the driver-expansion program design (umbrella: universal Discover-backed
browser + MTConnect, MQTT/Sparkplug B, BACnet/IP, SQL poll, Omron, Modbus RTU;
MELSEC deferred) plus the per-driver research reports.

All docs went through a 7-agent parallel review against the codebase before
this commit. Highlights fixed in review:

- universal browser: FOCAS FixedTree fills post-connect -> UntilStable settle
  + FixedTree.Enabled patch; MQTT reconciled to bespoke (was contradicting the
  program doc's SupportsOnlineDiscovery=false verdict)
- modbus-rtu: SerialPort.ReadTimeout doesn't bound async BaseStream reads ->
  linked-CTS per-op deadline (R2-01 class); BCL enum reuse would leak
  System.IO.Ports into Contracts
- bacnet: DiscoveryRediscoverPolicy enum name; UDP 47808 contention; live
  suite rewritten around unicast Who-Is + BBMD (broadcast doesn't cross VMs)
- sql-poll: real tier registration via DriverFactoryRegistry.Register;
  blackhole gate must not docker-pause the shared central SQL Server
- mqtt: Sparkplug v3.0 STATE topic form; first-in-repo proto codegen noted
- omron: host hardcodes isIdempotent:false today (retry seam unshipped);
  v1 scopes UDTs to dotted-leaf access
- mtconnect: SecurityClassification.ViewOnly; factory ParseEnum<T> pattern
- program doc: both valid enum-serialization patterns; IRediscoverable is
  change-signal-gated; RTU P2 adds System.IO.Ports; label is host-side
2026-07-15 16:40:36 -04:00

25 KiB
Raw Blame History

MTConnect (Agent-first) Driver — Research & Design

Status: Research / design proposal. No code written yet. Author: research pass, 2026-07-15. Scope: A new standard Equipment-kind driver (DriverType = "MTConnect") exposing an MTConnect Agent's data under the OtOpcUa unified address space — the same shape as Modbus / S7 / AbCip / TwinCAT / FOCAS / OpcUaClient.


0. TL;DR

  • Browseable: YES. MTConnect /probe returns a fully self-describing device model (Device → Components → DataItems) — a natural IDriverBrowser tree, closest in spirit to the OpcUaClient browser.
  • Recommended library: TrakHound MTConnect.NET (client packages), MIT-licensed. It is mature (v6.9.0.2, Oct 2025, 500k+ NuGet downloads), targets netstandard2.0 (so it loads on .NET 10) plus net6/7/8/9, and covers MTConnect versions up to 2.5. Hand-rolling is a viable fallback but not recommended for v1.
  • Read-only v1 (Discover + Read + Subscribe). No Write. MTConnect Interfaces (request/response write-back) exist but are rare, optional, and out of scope for v1.
  • Top risks: (1) MTConnect data is loosely typed — DataItems are strings with type/units metadata, so the OPC UA data-type mapping is a heuristic, not a guarantee; (2) CONDITION category has no clean scalar OPC UA analog and needs a modelling decision (fold to string vs. native Part 9 alarm).

1. Protocol summary + .NET library options

1.1 The two MTConnect layers

MTConnect (standard docs, SysML model v2.5) has two wire layers:

Layer Transport Device model? Role
Agent (primary) HTTP REST, XML (or JSON) Yes — self-describing The queryable server. What apps talk to.
Adapter / SHDR Raw pipe-delimited TCP (default :7878) No Feeds an Agent from a machine; no model, no discovery.

We target the Agent as the primary source. SHDR is a possible phase-2 ingest mode (§7) but loses auto-discovery.

1.2 Agent REST verbs

The Agent exposes three request types (MTConnect Standard Part 1, §5.4 / §8):

  • /probe (optionally /{device}/probe) → an MTConnectDevices response document: the Device Information Model — every Device, its nested Components, and each DataItem definition (id, type, category, subType, units, nativeUnits, name). This is the discovery + browse surface.
  • /current (optionally ?at=<sequence>) → an MTConnectStreams response document: a snapshot of the latest Observation for every DataItem at the moment of the request. This is the Read surface.
  • /sample?from=<seq>&count=<n>&interval=<ms> → an MTConnectStreams document containing a time-ordered set of observations since sequence from. This is the Subscribe surface.

Streaming / long-poll. When interval is supplied, the Agent holds the HTTP connection open and pushes successive MTConnectStreams chunks as a multipart/x-mixed-replace boundary stream — each chunk is one batch of new observations. Each response Header carries nextSequence; a poller issues the next /sample with from=nextSequence to get a contiguous, gap-free stream. The Agent's ring buffer has a finite bufferSize; if a consumer falls behind past the buffer, it must re-/current to re-baseline. (Sources: MTConnect.NET README, cppagent README, Part 1 Overview.)

1.3 The information model (probe)

Device → Component(s) → DataItem(s). A DataItem has three categories (from the SysML model):

  • SAMPLE — continuous numeric measurement over time. Types e.g. Position, Temperature, SpindleSpeed, PathFeedrate, Load, Acceleration. Carries units/nativeUnits.
  • EVENT — discrete state / value change. Types e.g. Availability (AVAILABLE/UNAVAILABLE), Execution (READY/ACTIVE/INTERRUPTED/STOPPED), ControllerMode (AUTOMATIC/MANUAL/...), Program, EmergencyStop, Block. Values are typically controlled vocabularies (enums) or free strings/ints.
  • CONDITION — health/alarm state. An observation is reported as one of Normal / Warning / Fault / Unavailable, with optional nativeCode, nativeSeverity, qualifier, and message text. Subtypes: Actuator, Communications, System, Temperature, LogicProgram, etc.

An observation in an MTConnectStreams document references its definition by dataItemId (and carries a sequence, timestamp, and value). This dataItemId is the stable per-tag address (§3).

1.4 .NET library options

Recommended: TrakHound MTConnect.NET client packages (MIT).

Fact Value Source
Latest version 6.9.0.2 (published 2025-10-16) NuGet MTConnect.NET-HTTP
License MIT (per latest NuGet package metadata) NuGet MTConnect.NET-HTTP
TFMs netstandard2.0, net6.0net9.0, net48 (runs on .NET 10 via netstandard2.0) README
MTConnect versions up to 2.5; auto-strips data not valid for requested version README
Maturity 500k+ NuGet downloads; actively maintained README

Packages we would consume (client side only — not the Agent/Adapter host packages):

  • MTConnect.NET-Common — model types (IDevice, IComponent, IDataItem, IObservation), the IMTConnectClient / IMTConnectEntityClient interfaces.
  • MTConnect.NET-HTTPMTConnectHttpClient: probe/current/sample with polling and streaming, gzip compression, XML and JSON. Exposes events for received documents (OnProbeReceived, OnCurrentReceived, OnSampleReceived, plus a per-observation callback) — a clean fit for pushing into ISubscribable.OnDataChange.
  • (MTConnect.NET-XML / -JSON are pulled transitively for serialization.)

⚠️ License due-diligence caveat: older release notes and the repo copyright header have shown mixed "MIT" / "Apache-2.0" / "© TrakHound, All Rights Reserved" strings across versions. The current NuGet package metadata for 6.9.0.2 declares MIT. Before taking the dependency, pin a specific version and confirm that exact version's embedded LICENSE / package license expression (a legal-review checkbox, not a blocker).

Fallback: hand-roll a minimal XML client. The Agent protocol is just three HTTP GETs returning XML. A hand-rolled client (HttpClient + System.Xml.Linq for probe, a multipart/x-mixed-replace boundary reader for sample) is ~300500 LoC and removes a third-party dependency + its transitive System.Text.Json version constraints. Recommendation: use the library for v1 (it handles version negotiation, the multipart framing, buffer-overflow re-baselining, and JSON/XML dual-format — all fiddly to reimplement correctly), and keep hand-roll as a contingency if the license review fails.


2. Capability mapping to the Equipment-kind seams

The driver implements the composable capability interfaces in src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/. Mapping:

OtOpcUa seam MTConnect mechanism Notes
IDriver (required) Construct MTConnectHttpClient from config, hold /probe model InitializeAsync does one probe; GetHealth reflects last successful poll + Agent instanceId
ITagDiscovery.DiscoverAsync Walk the /probe DeviceModel, stream Device→Component→DataItem into IAddressSpaceBuilder RediscoverPolicy = Once (probe is synchronous & complete). Redeploy on Agent instanceId change (see IRediscoverable, §below)
IReadable.ReadAsync(fullRefs) /current snapshot, index observations by dataItemId, return one DataValueSnapshot per requested ref Reads are idempotent (matches IReadable contract). Per-ref miss → Bad-coded snapshot, not a throw
ISubscribable.SubscribeAsync Start the MTConnectHttpClient sample stream (from/interval); fan each received observation to OnDataChange keyed by dataItemId One shared stream per driver instance (not per tag) — the Agent streams the whole device; filter to subscribed refs. Fire initial values from /current on subscribe (OPC UA convention)
IWritable NOT implemented (v1) See verdict below
IRediscoverable.OnRediscoveryNeeded Raise when the Agent's Header/@instanceId or @assetBufferSize changes (Agent restarted / model changed) → Host rebuilds address space Mirrors Galaxy's DeployWatcher pattern
IHostConnectivityProbe Cheap periodic /probe HEAD or /current to flip Running↔Stopped Mirrors ModbusProbeOptions
IAlarmSource (optional, phase 1.5) map CONDITION Fault/Warning → Part 9 native alarm See §2.3

2.1 Write verdict: read-only for v1

Justification:

  1. The mainstream MTConnect surface (/probe, /current, /sample) is strictly read-only by design — MTConnect was conceived as a read-only telemetry standard.
  2. Write-back exists only via MTConnect Interfaces (a request/response handshake modelled as its own DataItem categories — Request/Response events) which is optional, rarely deployed, and semantically a state-machine handshake, not a simple "set value." Modelling it as OPC UA IWritable would be misleading.
  3. The Equipment-kind seams are composable — a driver implements only what its backend supports (IDriver doc-comment). Omitting IWritable is idiomatic (Galaxy's write path is even fire-and-forget; several drivers are read-mostly). Nodes materialize without the AccessLevels.CurrentWrite bit.

Revisit Interfaces write-back in a later phase only if a concrete deployment needs it.

2.2 Data-type mapping (MTConnect → DriverDataType → OPC UA)

MTConnect DataItems are weakly typed on the wire (values arrive as strings). The mapping is inferred from category + type + units, and stored per-tag in the TagConfig (§3) so a wrong inference can be corrected by the author without a code change.

MTConnect Inferred DriverDataType Rationale
SAMPLE (numeric, has units) Float64 Continuous analog; Double is the safe superset
SAMPLE w/ representation="TIME_SERIES" Float64 array (ValueRank=1) Time-series sample bursts
EVENT controlled-vocab (Execution, ControllerMode, Availability, …) String Enum-like; keep the vocab string. (Optional: OPC UA enum modelling later)
EVENT numeric (e.g. PartCount, Line) Int64 Integer counters
EVENT free text (Program, Block, Message) String
CONDITION String (v1: the state word Normal/Warning/Fault/Unavailable) or native alarm (§2.3) No clean scalar analog
timestamp on every observation → OPC UA SourceTimestamp Not a separate node

DataValueSnapshot.StatusCode mapping: an observation value of UNAVAILABLE (MTConnect's explicit "no data" sentinel) → OPC UA Bad/Uncertain quality rather than a literal string, so downstream sees proper quality. Empty CONDITION or missing dataItem → Bad.

2.3 CONDITION modelling (decision point)

CONDITION is the awkward one. Two options:

  • v1 simple: expose each CONDITION DataItem as a String variable node whose value is the current condition state (Fault/Warning/Normal/Unavailable), optionally suffixed with nativeCode. Zero alarm plumbing.
  • v1.5 native: treat a Fault/Warning CONDITION as an OtOpcUa native alarm by emitting a TagConfig alarm object and implementing IAlarmSource — matching the Galaxy/Phase-B native-alarm pattern (route on the authored dotted reference, per the alarms memory). Higher value, more work.

Recommend v1 simple, v1.5 native as a fast-follow.


3. TagConfig JSON shape

Consistent with the Equipment-kind model: connection-level settings live in the driver config (agent base URL, auth, poll interval); per-tag addressing lives in TagConfig, and the platform FullName binds the tag to a driver-side reference. For MTConnect the natural stable reference is the dataItemId (globally unique within an Agent), so FullName = "<dataItemId>" (or, for readability, the driver can also accept a device/component/dataItemName path and resolve it to the id at discovery time).

3.1 Driver config (per driver instance) — MTConnectDriverOptions

{
  "AgentUri": "https://demo.mtconnect.org",   // Agent base URL (http or https)
  "DeviceName": "",                            // optional: scope to one device (else all devices)
  "PreferJson": false,                          // request application/json instead of XML if Agent supports it
  "SampleIntervalMs": 1000,                     // long-poll interval passed to /sample
  "SampleCount": 500,                           // max observations per sample chunk
  "HeartbeatMs": 10000,                         // Agent keep-alive heartbeat
  "RequestTimeoutMs": 30000,
  "AllowUntrustedTls": false,                   // dev/on-prem self-signed agents
  "Probe": { "Enabled": true, "IntervalMs": 5000 }  // IHostConnectivityProbe
}

3.2 Per-tag TagConfig (authored on the /uns Tags tab)

{
  "FullName": "avail",                       // <-- MTConnect dataItemId; the driver-side reference
  "mtDevice": "OKUMA.Lathe",                 // optional human context (device name from probe)
  "mtComponent": "Controller",               // optional
  "mtCategory": "EVENT",                     // SAMPLE | EVENT | CONDITION (from probe; drives typing)
  "mtType": "AVAILABILITY",                  // MTConnect DataItem type
  "mtSubType": null,
  "dataType": "String",                      // resolved DriverDataType (overridable by author)
  "units": null,                             // e.g. "CELSIUS" for a SAMPLE
  "writable": false                          // always false for MTConnect v1
}

A picked SAMPLE example:

{
  "FullName": "Xact",
  "mtDevice": "OKUMA.Lathe",
  "mtComponent": "Linear[X]",
  "mtCategory": "SAMPLE",
  "mtType": "POSITION",
  "mtSubType": "ACTUAL",
  "dataType": "Float64",
  "units": "MILLIMETER",
  "writable": false
}

The runtime driver resolves reads/subscriptions by FullName (= dataItemId) against the observation index it maintains from /current and /sample — exactly the "bind by TagConfig.FullName" pattern the other drivers use (EquipmentTagRefResolver<TDef>). Only FullName is load-bearing at runtime; the mt*/units fields are author-facing metadata + type inference inputs preserved across edits.


4. Browseability verdict + browse design

4.1 Verdict: YES — fully browseable.

/probe returns the complete MTConnectDevices model in one call — a static, self-describing tree. This is the cleanest possible browse source (better than Galaxy, which needs per-node expansion; comparable to a single-shot OPC UA browse). It maps directly onto IDriverBrowser/IBrowseSession.

4.2 New project: ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Browser

Mirror OpcUaClient.Browser layout exactly:

src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Browser/
  MTConnectDriverBrowser.cs     // IDriverBrowser: DriverType="MTConnect"; OpenAsync(configJson) -> session
  MTConnectBrowseSession.cs     // IBrowseSession over the fetched probe model
  ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Browser.csproj

Register in AdminUI DI alongside the others (src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/EndpointRouteBuilderExtensions.cs, ~line 49-50):

services.AddSingleton<IDriverBrowser, MTConnectDriverBrowser>();

4.3 Session behaviour (map to IBrowseSession)

Unlike OPC UA (live one-level-per-call), MTConnect probe is fetched once in OpenAsync and cached in the session; expansion is served from the in-memory tree (no further network calls). This is simpler and faster than the OpcUaClient browser.

Method Returns
OpenAsync(configJson) Deserialize MTConnectDriverOptions, GET {AgentUri}/probe, hold the parsed IDevice[] model in the session
RootAsync() One BrowseNode(Kind=Folder) per Device (or, if DeviceName is scoped, the top-level Components of that device)
ExpandAsync(nodeId) Children of a Device/Component: nested ComponentsFolder nodes; DataItemsLeaf nodes. NodeId encodes the path (e.g. dev:OKUMA.Lathe, comp:<componentId>, di:<dataItemId>)
AttributesAsync(nodeId) For a DataItem leaf, one-or-few AttributeInfo rows describing it (Name=dataItemName, DriverDataType=<inferred>, IsArray=<TIME_SERIES?>, SecurityClass, IsAlarm = (category==CONDITION)). For Device/Component folders, empty. This drives the picker's side-panel and lets a CONDITION pre-fill a native-alarm TagConfig (like Galaxy alarm attrs)
  • BrowseNode.NodeId for a DataItem leaf is di:<dataItemId> → the picker commits it, and the TagModal maps it to TagConfig.FullName = <dataItemId> plus the mt* metadata read from the cached probe model.
  • BrowseNodeKind.Leaf = DataItem (terminal, commit-on-select); Folder = Device/Component.
  • HasChildrenHint = true for Devices/Components that have child components or DataItems.

4.4 Picked-DataItem → TagConfig

When the picker commits a di:<dataItemId> leaf, the AdminUI builds the TagConfig in §3.2 from the cached probe DataItem: FullName=id, mtCategory/mtType/mtSubType/units copied verbatim, and dataType set from the §2.2 inference table (author can override in the typed editor).


5. Typed tag-editor (AdminUI)

To avoid the raw-JSON fallback, add a driver-typed editor (mirrors the driver-typed tag-editor pattern):

  • src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/MTConnectTagConfigModel.cs (pure FromJson/ToJson/Validate, preserves unknown keys) — copy the Modbus template.
  • .../Components/Shared/Uns/TagEditors/MTConnectTagConfigEditor.razor — a thin shell: fields for FullName (dataItemId), read-only mtCategory/mtType, and a dataType override dropdown.
  • Register ["MTConnect"] = typeof(...MTConnectTagConfigEditor) in Uns/TagEditors/TagConfigEditorMap.cs, and add validation in TagConfigValidator.

Because most tags are authored via the browse picker (which fills the model), the editor is mostly a confirm/override surface.


6. Test-fixture strategy

Ranked by value:

  1. Public demo Agents (fastest smoke path).

    • https://demo.mtconnect.org/ — the MTConnect Institute reference demo (probe/current/sample).
    • http://mtconnect.mazakcorp.com/ — Mazak demo agents.
    • NIST smstestbed publishes real agent configs + Devices.xml (usnistgov/smstestbed). These give a live, real-shape model for browse + read + streaming verification with zero infra. (Caveat: public agents are internet-dependent and not reproducible in CI.)
  2. Dockerized reference C++ Agent (reproducible integration fixture). The MTConnect reference Agent (mtconnect/cppagent) has an official Docker image. Add a fixture under tests/.../Docker/docker-compose.yml with the project: lmxopcua label (per the repo's Docker workflow), seeded with a canned Devices.xml and an SHDR simulator or the built-in agent adapter, exposed on the shared docker host 10.100.0.35. This is the integration-test analog of the Modbus/S7 sims.

  3. Canned probe/current/sample XML fixtures (unit tests). Capture one MTConnectDevices (probe) and a couple of MTConnectStreams (current + sample) XML documents from a demo agent into tests/.../MTConnect.Tests/Fixtures/. Drive the parser, type-inference, browse-tree, and observation-indexing logic with no network — the bulk of unit coverage. This is where the tricky bits (type inference, nextSequence paging, UNAVAILABLE→Bad, CONDITION handling, multipart chunk framing) get pinned.

Recommended: #3 for unit CI + #2 for the env-gated integration suite + #1 for manual live smoke.


7. Effort / risk / phasing

Phase 1 — Agent read + subscribe + browse (the MVP)

  • Projects: Driver.MTConnect (+ .Contracts for MTConnectDriverOptions/DTOs), Driver.MTConnect.Browser, MTConnect.Tests.
  • Implement IDriver + ITagDiscovery (probe→builder) + IReadable (current) + ISubscribable (sample stream) + IHostConnectivityProbe + IRediscoverable (instanceId change).
  • IDriverProbe for the AdminUI Test-Connect button (probe reachability).
  • IDriverBrowser/IBrowseSession from cached probe model; register in AdminUI DI.
  • Typed tag editor + TagConfigEditorMap/TagConfigValidator entries.
  • Register the factory in the Host bootstrapper (mirror ModbusDriverFactoryExtensions.Register).
  • CONDITION as String (simple).
  • Effort estimate: ~11.5 weeks with the TrakHound library (it removes the protocol grind). ~2.53 weeks hand-rolled.

Phase 1.5 (fast-follow)

  • CONDITION → native OPC UA Part 9 alarms via IAlarmSource (Galaxy native-alarm pattern).
  • TIME_SERIES SAMPLE arrays; EVENT controlled-vocab → OPC UA enumerations.

Phase 2 — SHDR adapter ingest (second source mode)

  • Add an optional SourceMode: "Agent" | "Shdr" to the driver config. In SHDR mode the driver opens the raw pipe-delimited TCP socket (default :7878) and parses <timestamp>|<key>|<value> lines.
  • Loses auto-discovery: SHDR has no device model, so ITagDiscovery cannot self-populate — tags must be manually authored (like Modbus), and the browser is unavailable in SHDR mode. Type inference falls back to author-supplied dataType.
  • Value: connect directly to a machine adapter with no Agent deployed. Niche; do only on demand.

Risk register

Risk Sev Mitigation
Weak wire typing → wrong OPC UA type inference Med Store resolved dataType in TagConfig; author-overridable; unit-test the inference table against real probe XML
CONDITION has no scalar analog Med v1 = String state; v1.5 = native alarm
TrakHound license string inconsistent across versions Low-Med Pin a version; confirm its embedded MIT license in legal review before merge
Public demo agents unreliable for CI Low Docker cppagent fixture + canned XML for CI; demo agents only for manual smoke
Agent ring-buffer overflow if consumer stalls Low Library re-baselines via /current on sequence gap; verify + test the fall-behind path
netstandard2.0-only lib on .NET 10 Low netstandard2.0 loads on net10.0; validate no trim/AOT issues at build

8. Source references

OtOpcUa code seams referenced

  • Capability interfaces: src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ (IDriver, ITagDiscovery, IReadable, ISubscribable, IWritable, IRediscoverable, IHostConnectivityProbe, IDriverFactory, DriverDataType).
  • Browse seam: src/Core/ZB.MOM.WW.OtOpcUa.Commons/Browsing/ (IDriverBrowser, IBrowseSession, BrowseNode, AttributeInfo).
  • Browser template: src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/.
  • Driver template: src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ (+ .Contracts), factory ModbusDriverFactoryExtensions.
  • AdminUI browser DI registration: src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/EndpointRouteBuilderExtensions.cs:49-50.
  • Typed tag editors: src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigEditorMap.cs + TagConfigValidator.cs.
  • Browse design doc: docs/plans/2026-05-28-driver-browsers-design.md.