71ccccef7c
- 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.
345 lines
20 KiB
Markdown
345 lines
20 KiB
Markdown
# 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`](../plans/2026-07-15-mtconnect-driver-design.md)
|
||
and the build-vs-plan record in
|
||
[`docs/plans/2026-07-24-mtconnect-driver.md`](../plans/2026-07-24-mtconnect-driver.md); for the
|
||
fixture recipe read
|
||
[`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/Docker/README.md`](../../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 `/run` verification 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` — a `multipart/x-mixed-replace` long-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`](../plans/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
|
||
`Component`s 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](#browse--free-via-the-universal-browser)
|
||
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](#deferred-not-in-this-build).
|
||
|
||
| 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](#known-limitations) |
|
||
| `IRediscoverable` | watches the Agent's `Header.instanceId` | No consumer wires it today — see [Known limitations](#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
|
||
|
||
```jsonc
|
||
"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:
|
||
|
||
1. The `RawTags[]` blob's `driverDataType` or `dataType` field (both
|
||
spellings accepted — the typed editor writes `dataType`, the driver's own
|
||
`tags[]` shape uses `driverDataType`).
|
||
2. A matching `Tags[]` entry naming the same DataItem id.
|
||
3. The Agent's own `/probe` declaration, via `MTConnectDataTypeInference.Infer`
|
||
(category/type/units/representation → `DriverDataType`).
|
||
4. `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](#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:
|
||
|
||
1. `IMTConnectAgentClient.IsSequenceGap` — the next chunk's `firstSequence`
|
||
is newer than the driver's expected cursor.
|
||
2. An `MTConnectError` document reporting `OUT_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](#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)
|
||
+ `DiscoverAsync` into a `CapturingAddressSpaceBuilder`, then tears the
|
||
throwaway instance down. Each captured leaf's NodeId is the DataItem `id`,
|
||
committed directly as `TagConfig.FullName` on pick. See
|
||
[`docs/plans/2026-07-15-universal-discovery-browser-design.md`](../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](#deferred-not-in-this-build).
|
||
|
||
## Known limitations
|
||
|
||
These are real, not placeholders — read them before relying on the driver
|
||
for anything beyond values-and-conditions.
|
||
|
||
1. **`IRediscoverable` and `IHostConnectivityProbe` have no consumer in the
|
||
server.** `DriverInstanceActor` wires only `ISubscribable.OnDataChange` and
|
||
`IAlarmSource.OnAlarmEvent`; nothing in the server subscribes to
|
||
`OnRediscoveryNeeded` or `OnHostStatusChanged` except `GalaxyDriver`
|
||
wiring its own internal `DeployWatcher` sub-component. So a restarted
|
||
Agent (a changed `instanceId`) 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.
|
||
2. **`RawTagEntry.DeviceName` is accepted on the wire shape but ignored for
|
||
routing.** One driver instance owns exactly one Agent client scoped by
|
||
`MTConnectDriverOptions.DeviceName` (the top-level config key); the
|
||
per-tag `RawTagEntry.DeviceName` field the v3 raw-tag identity carries is
|
||
never read by the driver. Two `/raw` Devices 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.
|
||
3. **Nothing validates a raw tag's declared OPC UA `DataType` against 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 authored `DataType` mismatched
|
||
against the actual Agent observation still surfaces as `BadTypeMismatch`
|
||
/`BadOutOfRange` at runtime rather than at authoring time.
|
||
4. **`sampleCount` is not a legal `DataItem` attribute.** It belongs on a
|
||
`TIME_SERIES` *observation*, not the device-model declaration. A real
|
||
Agent that meets `sampleCount` on a `DataItem` declaration logs `The
|
||
following keys were present and not expected: sampleCount` and **drops
|
||
the entire data item** from the served model. A live TIME_SERIES tag
|
||
therefore always resolves to `ArrayDim = null` (variable-length array) in
|
||
practice — the fixture's canned `probe.xml`/`Devices.xml`, which does
|
||
declare a `sampleCount`, is unrealistic on this point and exists only to
|
||
pin the parsing rule itself.
|
||
5. **CONDITION is a plain `String`, not a native Part 9 alarm** (see above).
|
||
6. **`TIME_SERIES` arrays and structured `DATA_SET`/`TABLE` observations
|
||
surface as `BadNotSupported`**, not an approximation — see the Quality
|
||
mapping table.
|
||
|
||
## Deferred (not in this build)
|
||
|
||
- **Write-back (MTConnect Interfaces).** Design
|
||
[§3.6](../plans/2026-07-15-mtconnect-driver-design.md#36-iwritable--not-implemented-v1):
|
||
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](../plans/2026-07-15-mtconnect-driver-design.md#9-phasing--effort)):
|
||
CONDITION → native OPC UA Part 9 alarms via `IAlarmSource` (the Galaxy
|
||
native-alarm pattern); `TIME_SERIES` SAMPLE 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, so
|
||
`SupportsOnlineDiscovery` would have to report `false` and 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 the `OUT_OF_RANGE` legs).
|
||
- **Integration tests** —
|
||
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/` — env-gated
|
||
on `MTCONNECT_AGENT_ENDPOINT` (default `http://10.100.0.35:5000`), skips
|
||
cleanly (12 Skipped) when the fixture is unreachable, 12/12 against a real
|
||
Agent. Read the fixture's own
|
||
[`Docker/README.md`](../../tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/Docker/README.md)
|
||
for the full recipe — summary only, here:
|
||
- Image is **`mtconnect/agent:2.7.0.12`** — not `mtconnect/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`, on `python:3.13-alpine`). An Agent with no adapter
|
||
reports every observation `UNAVAILABLE` forever, which proves nothing.
|
||
- Deployed at `/opt/otopcua-mtconnect/` on the shared docker host
|
||
(`10.100.0.35`, `project=lmxopcua` label). Endpoint
|
||
`http://10.100.0.35:5000`.
|
||
- `agent.cfg` must be **pure ASCII** — one non-ASCII byte anywhere,
|
||
including a comment, makes the config parser reject the whole file with
|
||
a bare `Failed / Stopped at line: N`.
|
||
- Bring-up: `lmxopcua-fix sync mtconnect` then `lmxopcua-fix up mtconnect`
|
||
(PowerShell helper, Windows-only) — or, directly on the docker host,
|
||
`rsync` the repo's `Docker/` dir to `/opt/otopcua-mtconnect/` and run
|
||
`docker compose up -d --wait`.
|
||
|
||
## Further reading
|
||
|
||
- [`docs/plans/2026-07-15-mtconnect-driver-design.md`](../plans/2026-07-15-mtconnect-driver-design.md) — full design: capability wiring, browse reconciliation, typed-editor spec, resilience/timeout rules, phasing
|
||
- [`docs/plans/2026-07-24-mtconnect-driver.md`](../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`](../plans/2026-07-24-driver-expansion-tracking.md) — Wave-2 program tracking
|
||
- [Docker fixture README](../../tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/Docker/README.md) — the authoritative fixture recipe (seeded device model, endpoint, Mac AirPlay port-5000 gotcha)
|