- docs/drivers/MTConnect.md: getting-started guide for the P1 Agent MVP — config keys read off MTConnectDriverOptions/ConfigDto, capability surface, RawPath->dataItemId coercion precedence, UNAVAILABLE->BadNoCommunication + the rest of the quality-code table, CONDITION-as-String, browse-via-the- universal-browser, the fixture recipe (links the existing IntegrationTests Docker README instead of duplicating it), and a Known limitations section covering the two pre-existing fleet-wide gaps this build surfaced (IRediscoverable/IHostConnectivityProbe have no server consumer; no driver form blocks Save on validation). Records write-back (MTConnect Interfaces), P1.5 (native alarms, TIME_SERIES arrays, EVENT->enum), and P2 (SHDR ingest) as deferred per design doc SS3.6/SS9. Documents where the build diverged from the design: TrakHound MTConnect.NET was dropped entirely (no XML formatter in the pinned packages, no injectable HTTP handler) in favor of a hand-rolled System.Xml.Linq client, namespace-agnostic on LocalName. - docs/drivers/README.md: adds MTConnect to the ground-truth driver table, the per-driver doc list, and the fixture coverage-map list. - docs/plans/2026-07-24-driver-expansion-tracking.md: marks the MTConnect Wave-2 row Done (22/23 tasks; Task 21 live gate tracked separately in the plan file, not touched here), corrects mtconnect/cppagent -> mtconnect/agent in the fixture note, and links the new driver guide. Deferred-cleanup decision (Task 1 review follow-up): removed GenerateDocumentationFile/NoWarn CS1591 from Driver.MTConnect.Contracts's csproj to match all eight sibling .Contracts projects, none of which carry it. Verified both the Contracts project alone and the full solution still build clean (0 warnings, 0 errors) with the flags gone — the existing XML doc comments don't depend on GenerateDocumentationFile to compile.
20 KiB
MTConnect Driver
Getting-started guide for the MTConnect Agent driver (P1 Agent MVP). This is
the short path — for the full design rationale read
docs/plans/2026-07-15-mtconnect-driver-design.md
and the build-vs-plan record in
docs/plans/2026-07-24-mtconnect-driver.md; for the
fixture recipe read
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/Docker/README.md
(not duplicated here).
Live gate: see the LIVE-GATE RESULT note in the plan's Task 21 (
docs/plans/2026-07-24-mtconnect-driver.md) for the docker-dev/runverification outcome (browse picker / typed editor / deploy / read / subscribe against the real Agent fixture).
What it talks to
A MTConnect Agent — the vendor-neutral read-only telemetry endpoint that front-ends a machine tool (or fronts an adapter that itself talks to the machine over SHDR). The driver speaks plain HTTP + XML against the Agent's three standard REST paths:
/probe— the static device model (Device → Component → DataItem tree)/current— a snapshot of every DataItem's latest observation/sample— amultipart/x-mixed-replacelong-poll stream of observation deltas, keyed by a monotonic sequence number
v1 is agent-first and read-only — Discover + Read + Subscribe, no Write — because the mainstream Agent surface has no "set value" operation.
Built vs. planned — read this before trusting the design doc's §2
The design doc (2026-07-15-mtconnect-driver-design.md)
picked the TrakHound MTConnect.NET-Common / -HTTP NuGet packages (MIT,
netstandard2.0) as the primary path, with a hand-rolled fallback. The
hand-rolled fallback is what shipped. Task 6/7 of the implementation plan
found by reflection + live invocation that the pinned TrakHound packages
ship no XML formatter (Document Formatter Not found for "xml" — the
formatter lives in a separate MTConnect.NET-XML package the design didn't
pin) and that MTConnectHttpClientStream exposes no injectable HttpClient
handler, so it cannot be unit-tested behind the driver's IMTConnectAgentClient
seam. Both package references were dropped. There is no TrakHound
dependency anywhere in the shipped driver — probe/current/sample parsing is
hand-rolled System.Xml.Linq plus a multipart boundary reader, entirely
behind IMTConnectAgentClient. See the CORRECTION block under Task 0 of the
implementation plan for the full record.
Namespace-agnostic parsing. Every parser matches XML elements on
LocalName only, ignoring the document's XML namespace entirely, so
MTConnect 1.3 through 2.x documents all parse without a schema-version
branch. This is deliberate, not sloppy: a real Agent injects vendor-extension
Components in a foreign namespace whose child DataItems elements inherit
the default namespace, and namespace-strict matching would silently drop
the whole vendor component (and every DataItem beneath it) rather than fail
loudly. Attributes are read unqualified for the same reason — so an
xsi:type attribute is never mistaken for a DataItem's own type.
Project split
| Project | Target | Role |
|---|---|---|
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/ |
net10.0 | In-process driver — MTConnectDriver, the hand-rolled IMTConnectAgentClient (probe/current/sample), the observation index, the factory |
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/ |
net10.0 | MTConnectDriverOptions, MTConnectTagDefinition, and the pure MTConnectDataTypeInference table shared by the driver, browse-commit, and the AdminUI typed editor |
No .Browser project — browse comes free from the Wave-0 universal
discovery browser (see Browse
below).
Capability surface
MTConnectDriver : IDriver, IReadable, ISubscribable, ITagDiscovery, IHostConnectivityProbe, IRediscoverable
(src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs).
Deliberately not IWritable — the Agent surface is read-only by design.
Write-back exists only via optional, rarely-deployed MTConnect Interfaces
(a request/response handshake, not a "set value" operation), and is out of
scope for this build — see Deferred.
| Capability | Path | Notes |
|---|---|---|
ITagDiscovery |
DiscoverAsync — streams /probe's Device→Component→DataItem tree into the address-space builder |
SupportsOnlineDiscovery = true, RediscoverPolicy = Once |
IReadable |
ReadAsync → one /current per call |
Not the production data path — see below |
ISubscribable |
SubscribeAsync/OnDataChange — the shared /sample long-poll pump |
The production data path |
IHostConnectivityProbe |
periodic /probe under Probe.* |
No consumer wires it today — see Known limitations |
IRediscoverable |
watches the Agent's Header.instanceId |
No consumer wires it today — see Known limitations |
IReadable.ReadAsync is never called by the running server. The
production data plane is entirely ISubscribable — DriverInstanceActor
subscribes once and lives off OnDataChange. ReadAsync exists for the
Client CLI (... read -n ...) and for the unit/integration suites; it is
correct and tested, just not on the hot path.
Minimum deployment
"Drivers": {
"mtconnect-1": {
"Type": "MTConnect",
"Config": {
"AgentUri": "http://10.100.0.35:5000",
"DeviceName": null,
"RequestTimeoutMs": 5000,
"SampleIntervalMs": 1000,
"SampleCount": 1000,
"HeartbeatMs": 10000,
"Probe": { "Enabled": true, "IntervalMs": 5000, "TimeoutMs": 2000 },
"Reconnect": { "MinBackoffMs": 0, "MaxBackoffMs": 30000, "BackoffMultiplier": 2.0 },
"Tags": []
}
}
}
RawTags[] is not authored by hand — DriverDeviceConfigMerger injects it
at deploy time from the tags authored on the /raw tree.
Config keys
Read off MTConnectDriverOptions / MTConnectDriverConfigDto
(src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectDriverOptions.cs,
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs):
| Key | Default | Notes |
|---|---|---|
AgentUri |
— (required) | Agent base URI; the driver appends /probe, /current, /sample |
DeviceName |
null |
Scopes requests to {AgentUri}/{DeviceName}/...; null = whole Agent |
RequestTimeoutMs |
5000 |
Per-call deadline for /probe and /current |
SampleIntervalMs |
1000 |
The Agent's /sample?interval= query param |
SampleCount |
1000 |
The Agent's /sample?count= query param |
HeartbeatMs |
10000 |
The Agent's /sample?heartbeat= query param — also the watchdog's liveness window |
Probe.Enabled / Probe.IntervalMs / Probe.TimeoutMs |
true / 5000 / 2000 |
Background connectivity-probe knobs (mirrors ModbusProbeOptions) |
Reconnect.MinBackoffMs / MaxBackoffMs / BackoffMultiplier |
0 / 30000 / 2.0 |
Geometric backoff after a failed request or a dropped /sample stream |
Tags[] |
[] |
Pre-v3 / CLI authoring surface — one MTConnectTagDefinition per DataItem id |
RawTags[] |
[] (deploy-injected) |
The v3 data-plane binding — see below |
All timing knobs are validated strictly positive at InitializeAsync
(RequirePositive, mirroring the arch-review 01/S-6 lesson: a 0 timeout
does not mean "wait forever," it faults the driver). This applies to
RequestTimeoutMs, HeartbeatMs, SampleIntervalMs, SampleCount, and
(when the probe is enabled) Probe.Interval / Probe.Timeout.
No save-time gate on a blank AgentUri in the AdminUI. The driver form
renders an inline validation notice (_form.Validate()), but
DriverConfigModal.SaveAsync has no per-form validation seam to block the
Save button on it — no sibling driver form has one either. A blank
AgentUri saves cleanly and fails at deploy with the driver's own error
message, not at authoring time.
Data plane
FullName == DataItem id
FullName (both MTConnectTagDefinition.FullName and every RawTags[]
blob's identifier) is the MTConnect DataItem@id attribute — the value the
universal browser commits and the key the driver resolves reads/subscribes
against. MTConnectTagConfigModel.FromJson (the AdminUI typed editor) tries
three spellings in order — fullName → dataItemId → address — and takes
the first non-blank one, normalising onto fullName on save. address is
accepted because that's the field name RawBrowseCommitMapper writes for a
driver with no typed editor path, so a browse-committed tag stays readable
even before the editor round-trips it.
Coercion-type precedence
The driver keys its data plane by RawPath (the v3 raw-tag identity), not
by DataItem id directly, resolving RawPath → dataItemId internally. Each raw
tag's coercion type (the DriverDataType its Agent observation is parsed
into) is resolved in this order, first non-null wins:
- The
RawTags[]blob'sdriverDataTypeordataTypefield (both spellings accepted — the typed editor writesdataType, the driver's owntags[]shape usesdriverDataType). - A matching
Tags[]entry naming the same DataItem id. - The Agent's own
/probedeclaration, viaMTConnectDataTypeInference.Infer(category/type/units/representation →DriverDataType). DriverDataType.String— the coercion that cannot fail.
Quality mapping
UNAVAILABLE — MTConnect's one explicit "I have no value" sentinel — maps to
BadNoCommunication (0x80310000). This is deliberately distinct from
the fleet-standard BadCommunicationError (0x80050000, used elsewhere for
the driver's own transport failure to the Agent): UNAVAILABLE means the
Agent is reachable and answered, it just has no device-backed value for that
item. A CONDITION observation's value is taken from its element name
(<Normal/> → the string "Normal", <Fault/> → "Fault"); a <Unavailable/>
condition element normalises onto the same UNAVAILABLE sentinel as every
other category so one comparison covers all three.
Other status codes an observation can surface:
| Code | Meaning |
|---|---|
BadTypeMismatch (0x80740000) |
The Agent's text isn't a value of the tag's coerced type at all |
BadOutOfRange (0x803C0000) |
The Agent reported a number the coerced type can't represent |
BadNotSupported (0x803D0000) |
A shape this build doesn't materialize — a TIME_SERIES vector, or a structured DATA_SET/TABLE observation (deferred to P1.5; see Known limitations) |
BadWaitingForInitialData |
Tag is authored but the Agent hasn't reported it yet |
BadNodeIdUnknown |
DataItem id is neither authored nor ever observed |
Subscribe — the /sample pump
One shared /sample long-poll stream per driver instance (the Agent streams
the whole device model regardless of which subset is subscribed, so
per-tag streams would be wasted round-trips), run under a heartbeat
watchdog — HttpClient.Timeout cannot bound a long-lived stream, so a
missing chunk and missing keep-alive heartbeat within
HeartbeatMs × N is what detects a frozen peer.
Ring-buffer overflow (the Agent's circular observation buffer wrapped past
what the driver's cursor expects) is detected two ways, both triggering a
/current re-baseline before the stream resumes:
IMTConnectAgentClient.IsSequenceGap— the next chunk'sfirstSequenceis newer than the driver's expected cursor.- An
MTConnectErrordocument reportingOUT_OF_RANGE— real Agents return this both under HTTP 200 (a normal MTConnect error document) and as a bare HTTP 400, and the client handles both.
An Agent instanceId change means the Agent restarted, and is checked
before the sequence-gap check (a restart also usually trips the gap, so
the two need disambiguating, not just OR-ing together). On a changed
instanceId the driver clears its cached probe model and raises
OnRediscoveryNeeded — see Known limitations for why
that signal currently has no consumer.
Browse — free via the universal browser
MTConnect ships no bespoke browser project. Setting
ITagDiscovery.SupportsOnlineDiscovery => true is the entire integration:
the Wave-0 DiscoveryDriverBrowser sees a driver whose TryCreate succeeds
and whose instance reports online discovery, renders the AdminUI Browse
button, constructs the driver, runs InitializeAsync (the /probe connect)
DiscoverAsyncinto aCapturingAddressSpaceBuilder, then tears the throwaway instance down. Each captured leaf's NodeId is the DataItemid, committed directly asTagConfig.FullNameon pick. Seedocs/plans/2026-07-15-universal-discovery-browser-design.md.
CONDITION modelling (v1: plain String)
Each CONDITION DataItem materializes as a String variable whose value is
the current state word — Normal / Warning / Fault / UNAVAILABLE.
There is no native OPC UA Part 9 alarm plumbing in this build;
DriverAttributeInfo.IsAlarm = true is still stamped on a CONDITION leaf so
the browse side-panel flags it and a future upgrade to native alarms doesn't
need re-authoring. See Deferred.
Known limitations
These are real, not placeholders — read them before relying on the driver for anything beyond values-and-conditions.
IRediscoverableandIHostConnectivityProbehave no consumer in the server.DriverInstanceActorwires onlyISubscribable.OnDataChangeandIAlarmSource.OnAlarmEvent; nothing in the server subscribes toOnRediscoveryNeededorOnHostStatusChangedexceptGalaxyDriverwiring its own internalDeployWatchersub-component. So a restarted Agent (a changedinstanceId) leaves a stale address space behind an otherwise-Healthy driver. This is a pre-existing fleet-wide gap affecting every driver that implements either interface (eight drivers besides Galaxy), not something specific to MTConnect — this build simply surfaced it again.RawTagEntry.DeviceNameis accepted on the wire shape but ignored for routing. One driver instance owns exactly one Agent client scoped byMTConnectDriverOptions.DeviceName(the top-level config key); the per-tagRawTagEntry.DeviceNamefield the v3 raw-tag identity carries is never read by the driver. Two/rawDevices authored under one MTConnect driver instance both resolve to the same one Agent connection. Real per-device routing (a client per device) would be a design change, not a bug fix.- Nothing validates a raw tag's declared OPC UA
DataTypeagainst the blob's coercion type. The/probe-sourced inference (MTConnectDataTypeInference) narrows how often an author picks a wrong type, but it doesn't close the gap — an authoredDataTypemismatched against the actual Agent observation still surfaces asBadTypeMismatch/BadOutOfRangeat runtime rather than at authoring time. sampleCountis not a legalDataItemattribute. It belongs on aTIME_SERIESobservation, not the device-model declaration. A real Agent that meetssampleCounton aDataItemdeclaration logsThe following keys were present and not expected: sampleCountand drops the entire data item from the served model. A live TIME_SERIES tag therefore always resolves toArrayDim = null(variable-length array) in practice — the fixture's cannedprobe.xml/Devices.xml, which does declare asampleCount, is unrealistic on this point and exists only to pin the parsing rule itself.- CONDITION is a plain
String, not a native Part 9 alarm (see above). TIME_SERIESarrays and structuredDATA_SET/TABLEobservations surface asBadNotSupported, not an approximation — see the Quality mapping table.
Deferred (not in this build)
- Write-back (MTConnect Interfaces). Design
§3.6:
the mainstream Agent surface is read-only by design; Interfaces is a rare,
optional request/response handshake that would mislead if modelled as
IWritable. Revisit only on a concrete deployment need. - P1.5 fast-follow (design
§9):
CONDITION → native OPC UA Part 9 alarms via
IAlarmSource(the Galaxy native-alarm pattern);TIME_SERIESSAMPLE arrays materialized as real OPC UA arrays; EVENT controlled-vocabulary values → OPC UA enumerations. - P2 (on demand): SHDR adapter ingest. A
SourceMode: "Agent" | "Shdr"switch that opens the raw pipe-delimited SHDR TCP socket directly. Loses auto-discovery (no device model without an Agent in front, soSupportsOnlineDiscoverywould have to reportfalseand tags would be authored by hand, like Modbus). Niche; not built.
Testing
- Unit tests —
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/(canned-XML fixtures, no network) — 491/491 at time of writing. Covers discovery tree shape,MTConnectDataTypeInference, observation indexing,UNAVAILABLE→BadNoCommunication, CONDITION state-word mapping, multipart chunk framing, and ring-buffer re-baseline paging (both the sequence-gap and theOUT_OF_RANGElegs). - Integration tests —
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/— env-gated onMTCONNECT_AGENT_ENDPOINT(defaulthttp://10.100.0.35:5000), skips cleanly (12 Skipped) when the fixture is unreachable, 12/12 against a real Agent. Read the fixture's ownDocker/README.mdfor the full recipe — summary only, here:- Image is
mtconnect/agent:2.7.0.12— notmtconnect/cppagent, which does not exist on Docker Hub (the design's original assumption). - The stack is two services: the Agent plus a stdlib-only SHDR adapter
(
Docker/adapter.py, onpython:3.13-alpine). An Agent with no adapter reports every observationUNAVAILABLEforever, which proves nothing. - Deployed at
/opt/otopcua-mtconnect/on the shared docker host (10.100.0.35,project=lmxopcualabel). Endpointhttp://10.100.0.35:5000. agent.cfgmust be pure ASCII — one non-ASCII byte anywhere, including a comment, makes the config parser reject the whole file with a bareFailed / Stopped at line: N.- Bring-up:
lmxopcua-fix sync mtconnectthenlmxopcua-fix up mtconnect(PowerShell helper, Windows-only) — or, directly on the docker host,rsyncthe repo'sDocker/dir to/opt/otopcua-mtconnect/and rundocker compose up -d --wait.
- Image is
Further reading
docs/plans/2026-07-15-mtconnect-driver-design.md— full design: capability wiring, browse reconciliation, typed-editor spec, resilience/timeout rules, phasingdocs/plans/2026-07-24-mtconnect-driver.md— the executable implementation plan and the task-by-task build record, including where it diverged from the design (TrakHound → hand-rolled)docs/plans/2026-07-24-driver-expansion-tracking.md— Wave-2 program tracking- Docker fixture README — the authoritative fixture recipe (seeded device model, endpoint, Mac AirPlay port-5000 gotcha)