Files
lmxopcua/docs/research/drivers/mtconnect-agent.md
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

409 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](https://docs.mtconnect.org/), [SysML model v2.5](https://model.mtconnect.org/Version2.5/Fundamentals/))
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](https://github.com/TrakHound/MTConnect.NET/blob/master/README.md),
[cppagent README](https://github.com/mtconnect/cppagent/blob/master/README.md),
[Part 1 Overview](https://docs.mtconnect.org/MTC_Part_1_Overview+V1.2.pdf).)
### 1.3 The information model (probe)
`Device → Component(s) → DataItem(s)`. A `DataItem` has three **categories** (from the
[SysML model](https://model.mtconnect.org/Version2.5/Fundamentals/)):
- **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](https://www.nuget.org/packages/MTConnect.NET-HTTP/) |
| License | **MIT** (per latest NuGet package metadata) | [NuGet MTConnect.NET-HTTP](https://www.nuget.org/packages/MTConnect.NET-HTTP/) |
| TFMs | netstandard2.0, net6.0net9.0, net48 (**runs on .NET 10 via netstandard2.0**) | [README](https://github.com/TrakHound/MTConnect.NET/blob/master/README.md) |
| MTConnect versions | up to 2.5; auto-strips data not valid for requested version | [README](https://github.com/TrakHound/MTConnect.NET/blob/master/README.md) |
| Maturity | 500k+ NuGet downloads; actively maintained | [README](https://github.com/TrakHound/MTConnect.NET/blob/master/README.md) |
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-HTTP`** — `MTConnectHttpClient`: 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`
```jsonc
{
"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)
```jsonc
{
"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:
```jsonc
{
"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):
```csharp
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 **Components**`Folder` nodes; **DataItems**`Leaf` 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](https://github.com/usnistgov/smstestbed/blob/master/mtconnect/agent/Devices.xml)).
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](https://github.com/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
- MTConnect Standard Part 1 (Overview & Protocol): https://docs.mtconnect.org/MTC_Part_1_Overview+V1.2.pdf
- MTConnect SysML Model v2.5 (Fundamentals — Device/Component/DataItem, categories): https://model.mtconnect.org/Version2.5/Fundamentals/
- TrakHound MTConnect.NET (README, client API, TFMs, versions): https://github.com/TrakHound/MTConnect.NET/blob/master/README.md
- NuGet MTConnect.NET-HTTP (license=MIT, v6.9.0.2 2025-10-16, TFMs, deps): https://www.nuget.org/packages/MTConnect.NET-HTTP/
- MTConnect reference C++ Agent (Docker fixture): https://github.com/mtconnect/cppagent/blob/master/README.md
- NIST smstestbed agent config + Devices.xml (real fixtures): https://github.com/usnistgov/smstestbed/blob/master/mtconnect/agent/Devices.xml
- Public demo agent: https://demo.mtconnect.org/ · Mazak demo agents: http://mtconnect.mazakcorp.com/
### 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`.