feat(mtconnect): read-only MTConnect Agent driver (P1) — 23 tasks + 5-leg live gate #506
@@ -0,0 +1,344 @@
|
|||||||
|
# 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)
|
||||||
@@ -30,6 +30,7 @@ Driver type metadata is registered at startup in `DriverTypeRegistry` (`src/Core
|
|||||||
| [FOCAS](FOCAS.md) | `Driver.FOCAS` | A | Pure-managed `FocasWireClient` — FOCAS/2 Ethernet binary protocol on TCP:8193, inlined into the driver assembly | IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IAlarmSource | `IWritable` is implemented but read-only by design — `WriteAsync` returns `BadNotWritable` for every point. CNC-shaped data model (axes, spindle, PMC, macros, alarms) not a flat tag map. Previously Tier-C (Host + P/Invoke + shim DLL); retired in the 2026-04-24 migration when the managed wire client landed |
|
| [FOCAS](FOCAS.md) | `Driver.FOCAS` | A | Pure-managed `FocasWireClient` — FOCAS/2 Ethernet binary protocol on TCP:8193, inlined into the driver assembly | IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IAlarmSource | `IWritable` is implemented but read-only by design — `WriteAsync` returns `BadNotWritable` for every point. CNC-shaped data model (axes, spindle, PMC, macros, alarms) not a flat tag map. Previously Tier-C (Host + P/Invoke + shim DLL); retired in the 2026-04-24 migration when the managed wire client landed |
|
||||||
| [OPC UA Client](OpcUaClient.md) | `Driver.OpcUaClient` | B | OPCFoundation `Opc.Ua.Client` | IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IAlarmSource, IHistoryProvider, IHostConnectivityProbe | Gateway/aggregation driver — the only driver implementing driver-side `IHistoryProvider` (forwards HistoryRead to the upstream server). Opens a single `Session` against a remote OPC UA server and re-exposes its address space. Owns its own `ApplicationConfiguration` (distinct from `Client.Shared`) because it's always-on with keep-alive + `TransferSubscriptions` across SDK reconnect, not an interactive CLI |
|
| [OPC UA Client](OpcUaClient.md) | `Driver.OpcUaClient` | B | OPCFoundation `Opc.Ua.Client` | IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IAlarmSource, IHistoryProvider, IHostConnectivityProbe | Gateway/aggregation driver — the only driver implementing driver-side `IHistoryProvider` (forwards HistoryRead to the upstream server). Opens a single `Session` against a remote OPC UA server and re-exposes its address space. Owns its own `ApplicationConfiguration` (distinct from `Client.Shared`) because it's always-on with keep-alive + `TransferSubscriptions` across SDK reconnect, not an interactive CLI |
|
||||||
| [MQTT](Mqtt.md) | `Driver.Mqtt` (+ `.Browser`, `.Contracts`) | A | [MQTTnet 5](https://github.com/dotnet/MQTTnet) | IDriver, ITagDiscovery, IReadable, ISubscribable, IHostConnectivityProbe, IRediscoverable | **Push, not poll, and read-only** — the broker delivers PUBLISH frames the driver forwards straight to `ISubscribable.OnDataChange`; `IReadable` serves the last retained/received value from cache, and there is **no `IWritable`** (nothing publishes back). Subscriptions are indexed **by topic filter, not by topic**, so wildcard tags survive a reconnect. A rejected CONNACK throws `MqttConnectRejectedException`: unrecoverable codes → `Faulted` + supervisor stop, transient → retry. **Two modes:** `Plain` binds a tag to a concrete topic + JSON path; `SparkplugB` decodes vendored Eclipse-Tahu protobuf under one `spBv1.0/{groupId}/#` subscription and binds by the `group/edgeNode/device?/metric` tuple (birth/alias/seq-gap state machine, death→STALE, scoped rebirth NCMD). `IRediscoverable` fires on a DBIRTH but is **inert platform-side** — see [#507](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/507) |
|
| [MQTT](Mqtt.md) | `Driver.Mqtt` (+ `.Browser`, `.Contracts`) | A | [MQTTnet 5](https://github.com/dotnet/MQTTnet) | IDriver, ITagDiscovery, IReadable, ISubscribable, IHostConnectivityProbe, IRediscoverable | **Push, not poll, and read-only** — the broker delivers PUBLISH frames the driver forwards straight to `ISubscribable.OnDataChange`; `IReadable` serves the last retained/received value from cache, and there is **no `IWritable`** (nothing publishes back). Subscriptions are indexed **by topic filter, not by topic**, so wildcard tags survive a reconnect. A rejected CONNACK throws `MqttConnectRejectedException`: unrecoverable codes → `Faulted` + supervisor stop, transient → retry. **Two modes:** `Plain` binds a tag to a concrete topic + JSON path; `SparkplugB` decodes vendored Eclipse-Tahu protobuf under one `spBv1.0/{groupId}/#` subscription and binds by the `group/edgeNode/device?/metric` tuple (birth/alias/seq-gap state machine, death→STALE, scoped rebirth NCMD). `IRediscoverable` fires on a DBIRTH but is **inert platform-side** — see [#507](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/507) |
|
||||||
|
| [MTConnect](MTConnect.md) | `Driver.MTConnect` (+ `.Contracts`) | — | Hand-rolled `System.Xml.Linq` HTTP/XML client against a vendor-neutral MTConnect Agent (`/probe`, `/current`, `/sample`) — no TrakHound dependency (dropped; see the driver doc) | IDriver, ITagDiscovery, IReadable, ISubscribable, IHostConnectivityProbe, IRediscoverable | Read-only by design (no `IWritable`). Production data plane is entirely `ISubscribable`/`OnDataChange` — `IReadable` is CLI/test-only. Browse is free via the Wave-0 universal discovery browser, no bespoke browser project. `IRediscoverable`/`IHostConnectivityProbe` have no server-side consumer yet — a pre-existing fleet-wide gap, not MTConnect-specific |
|
||||||
| [Historian.Gateway](../Historian.md) | `Driver.Historian.Gateway` | — | `ZB.MOM.WW.HistorianGateway.Client` gRPC (`historian_gateway.v1`) | IHistorianDataSource (server-side read backend) + alarm `SendEvent` writer + `WriteLiveValues` recorder + `IHistorianProvisioning` | Not a tag driver — the sole historian backend. Registers `GatewayHistorianDataSource : IHistorianDataSource` for HistoryRead and serves alarm-write + continuous historization through the gateway. No `IDriver`/`ITagDiscovery` surface. (The retired Wonderware sidecar backend it replaced is documented at [Historian.Wonderware.md](Historian.Wonderware.md).) |
|
| [Historian.Gateway](../Historian.md) | `Driver.Historian.Gateway` | — | `ZB.MOM.WW.HistorianGateway.Client` gRPC (`historian_gateway.v1`) | IHistorianDataSource (server-side read backend) + alarm `SendEvent` writer + `WriteLiveValues` recorder + `IHistorianProvisioning` | Not a tag driver — the sole historian backend. Registers `GatewayHistorianDataSource : IHistorianDataSource` for HistoryRead and serves alarm-write + continuous historization through the gateway. No `IDriver`/`ITagDiscovery` surface. (The retired Wonderware sidecar backend it replaced is documented at [Historian.Wonderware.md](Historian.Wonderware.md).) |
|
||||||
|
|
||||||
## Per-driver documentation
|
## Per-driver documentation
|
||||||
@@ -50,6 +51,10 @@ Driver type metadata is registered at startup in `DriverTypeRegistry` (`src/Core
|
|||||||
- [OpcUaClient.md](OpcUaClient.md) — OPC UA Client (gateway/aggregation) driver: remote-server session, driver-side HistoryRead forwarding, reconnect behaviour
|
- [OpcUaClient.md](OpcUaClient.md) — OPC UA Client (gateway/aggregation) driver: remote-server session, driver-side HistoryRead forwarding, reconnect behaviour
|
||||||
- [Mqtt.md](Mqtt.md) — MQTT broker-subscribe driver: broker/TLS/auth config, topic + JSON-path tag binding, retained-value seeding, `#`-observation browser, CONNACK-rejection semantics, **Sparkplug B** (birth/alias/seq/rebirth state machine, birth-driven browse picker, death→STALE), and the **Known gaps** table
|
- [Mqtt.md](Mqtt.md) — MQTT broker-subscribe driver: broker/TLS/auth config, topic + JSON-path tag binding, retained-value seeding, `#`-observation browser, CONNACK-rejection semantics, **Sparkplug B** (birth/alias/seq/rebirth state machine, birth-driven browse picker, death→STALE), and the **Known gaps** table
|
||||||
|
|
||||||
|
- **MTConnect** has a short getting-started doc because the hand-rolled-vs-TrakHound divergence, the
|
||||||
|
data-plane precedence rules, and a non-trivial known-limitations list need explaining up front:
|
||||||
|
- [MTConnect.md](MTConnect.md) — Agent config, `FullName`==DataItem `id`, `UNAVAILABLE`→`BadNoCommunication`, CONDITION-as-`String`, browse-via-universal-browser, fixture recipe, deferred write-back
|
||||||
|
|
||||||
- **Historian.Gateway** (server-side historian backend, not a tag driver) is documented in the main guide:
|
- **Historian.Gateway** (server-side historian backend, not a tag driver) is documented in the main guide:
|
||||||
- [../Historian.md](../Historian.md) — HistorianGateway backend: read-path registration, HistoryRead dispatch, alarm store-and-forward (`SendEvent`), continuous historization (`WriteLiveValues`), `EnsureTags` provisioning, config keys, deployment prerequisites. (The retired Wonderware sidecar backend it replaced: [Historian.Wonderware.md](Historian.Wonderware.md).)
|
- [../Historian.md](../Historian.md) — HistorianGateway backend: read-path registration, HistoryRead dispatch, alarm store-and-forward (`SendEvent`), continuous historization (`WriteLiveValues`), `EnsureTags` provisioning, config keys, deployment prerequisites. (The retired Wonderware sidecar backend it replaced: [Historian.Wonderware.md](Historian.Wonderware.md).)
|
||||||
|
|
||||||
@@ -68,6 +73,7 @@ Each driver has a dedicated fixture doc that lays out what the integration / uni
|
|||||||
- [OPC UA Client](OpcUaClient-Test-Fixture.md) — Dockerized `opc-plc` integration suite (task #215): real Secure Channel + Session, read + subscribe verified end-to-end; write not yet exercised in the integration suite; exhaustive capability matrix (reconnect, failover, cert-auth, history, alarms) via unit suite with mocked `Session`
|
- [OPC UA Client](OpcUaClient-Test-Fixture.md) — Dockerized `opc-plc` integration suite (task #215): real Secure Channel + Session, read + subscribe verified end-to-end; write not yet exercised in the integration suite; exhaustive capability matrix (reconnect, failover, cert-auth, history, alarms) via unit suite with mocked `Session`
|
||||||
- [MQTT](Mqtt-Test-Fixture.md) — Dockerized `eclipse-mosquitto` with **TLS + real auth on both listeners (no anonymous fallback)** + a JSON-publisher sidecar; env-gated live suite (**15 tests**) — 7 plain (TLS/auth connect, CA pinning both ways, subscribe→value, retained seeding, wrong-password rejection) + 8 Sparkplug against a **project-owned C# edge-node simulator** on the `sparkplug` compose profile (birth→alias bind, DDATA, NDEATH→STALE, rebirth NCMD round-trip). Births are never retained, so `docker restart otopcua-sparkplug-sim` is how you force a fresh one
|
- [MQTT](Mqtt-Test-Fixture.md) — Dockerized `eclipse-mosquitto` with **TLS + real auth on both listeners (no anonymous fallback)** + a JSON-publisher sidecar; env-gated live suite (**15 tests**) — 7 plain (TLS/auth connect, CA pinning both ways, subscribe→value, retained seeding, wrong-password rejection) + 8 Sparkplug against a **project-owned C# edge-node simulator** on the `sparkplug` compose profile (birth→alias bind, DDATA, NDEATH→STALE, rebirth NCMD round-trip). Births are never retained, so `docker restart otopcua-sparkplug-sim` is how you force a fresh one
|
||||||
- [Galaxy](../v1/drivers/Galaxy-Test-Fixture.md) — richest harness: gateway E2E + ZB SQL live-smoke + MXAccess opt-in (v1 archive)
|
- [Galaxy](../v1/drivers/Galaxy-Test-Fixture.md) — richest harness: gateway E2E + ZB SQL live-smoke + MXAccess opt-in (v1 archive)
|
||||||
|
- [MTConnect](../../tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/Docker/README.md) — Dockerized `mtconnect/agent` + stdlib SHDR adapter (two services, the Agent alone reports everything `UNAVAILABLE`); 12/12 against the real Agent, canned XML unit fixtures cover the bulk (491/491)
|
||||||
|
|
||||||
## Related cross-driver docs
|
## Related cross-driver docs
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ it reaches 📝.
|
|||||||
| **0** | Universal Discover-backed browser | [design](2026-07-15-universal-discovery-browser-design.md) | [plan](2026-07-15-universal-discovery-browser-implementation.md) · [tasks](2026-07-15-universal-discovery-browser-implementation.md.tasks.json) | 🟡 **Live gate open** | S–M | none (retrofits shipped drivers) |
|
| **0** | Universal Discover-backed browser | [design](2026-07-15-universal-discovery-browser-design.md) | [plan](2026-07-15-universal-discovery-browser-implementation.md) · [tasks](2026-07-15-universal-discovery-browser-implementation.md.tasks.json) | 🟡 **Live gate open** | S–M | none (retrofits shipped drivers) |
|
||||||
| **1** | Modbus RTU (extend Modbus) | [design](2026-07-15-modbus-rtu-driver-design.md) | [plan](2026-07-24-modbus-rtu-driver.md) · [tasks](2026-07-24-modbus-rtu-driver.md.tasks.json) | 🟢 **Built — live gate PASSED** (11 tasks, branch `feat/modbus-rtu-driver`, pending merge) | **S (lowest)** | `pymodbus` `rtu_over_tcp` — CI-simulatable, no new infra |
|
| **1** | Modbus RTU (extend Modbus) | [design](2026-07-15-modbus-rtu-driver-design.md) | [plan](2026-07-24-modbus-rtu-driver.md) · [tasks](2026-07-24-modbus-rtu-driver.md.tasks.json) | 🟢 **Built — live gate PASSED** (11 tasks, branch `feat/modbus-rtu-driver`, pending merge) | **S (lowest)** | `pymodbus` `rtu_over_tcp` — CI-simulatable, no new infra |
|
||||||
| **1** | SQL poll | [design](2026-07-15-sql-poll-driver-design.md) | [plan](2026-07-24-sql-poll-driver.md) · [tasks](2026-07-24-sql-poll-driver.md.tasks.json) | 📝 **Plan ready** (22 tasks) | S–M | **existing central SQL Server** `10.100.0.35,14330` + SQLite unit |
|
| **1** | SQL poll | [design](2026-07-15-sql-poll-driver-design.md) | [plan](2026-07-24-sql-poll-driver.md) · [tasks](2026-07-24-sql-poll-driver.md.tasks.json) | 📝 **Plan ready** (22 tasks) | S–M | **existing central SQL Server** `10.100.0.35,14330` + SQLite unit |
|
||||||
| **2** | MTConnect Agent | [design](2026-07-15-mtconnect-driver-design.md) | [plan](2026-07-24-mtconnect-driver.md) · [tasks](2026-07-24-mtconnect-driver.md.tasks.json) | 📝 **Plan ready** (23 tasks) | S–M | Dockerized `mtconnect/cppagent` — CI-simulatable |
|
| **2** | MTConnect Agent | [design](2026-07-15-mtconnect-driver-design.md) | [plan](2026-07-24-mtconnect-driver.md) · [tasks](2026-07-24-mtconnect-driver.md.tasks.json) | ✅ **COMPLETE** — P1 Agent MVP, all 23 tasks + the 5-leg live gate PASSED (branch `feat/mtconnect-driver`, PR #506) | S–M | Dockerized `mtconnect/agent:2.7.0.12` (not `cppagent`) — CI-simulatable, live at `10.100.0.35:5000` |
|
||||||
| **2** | MQTT / Sparkplug B | [design](2026-07-15-mqtt-sparkplug-driver-design.md) | [plan](2026-07-24-mqtt-sparkplug-driver.md) · [tasks](2026-07-24-mqtt-sparkplug-driver.md.tasks.json) | ✅ **COMPLETE** — P1 + P2 shipped, both live-gated (Tasks 0–26) | M–L | Mosquitto TLS+auth **+ C# Sparkplug edge-node simulator**, live at `10.100.0.35:8883` |
|
| **2** | MQTT / Sparkplug B | [design](2026-07-15-mqtt-sparkplug-driver-design.md) | [plan](2026-07-24-mqtt-sparkplug-driver.md) · [tasks](2026-07-24-mqtt-sparkplug-driver.md.tasks.json) | ✅ **COMPLETE** — P1 + P2 shipped, both live-gated (Tasks 0–26) | M–L | Mosquitto TLS+auth **+ C# Sparkplug edge-node simulator**, live at `10.100.0.35:8883` |
|
||||||
|
|
||||||
> Waves 3+ (BACnet/IP, Omron) and the deferred MELSEC are tracked in the program design doc §6/§8,
|
> Waves 3+ (BACnet/IP, Omron) and the deferred MELSEC are tracked in the program design doc §6/§8,
|
||||||
@@ -134,15 +134,36 @@ SQL poll reuses the always-on central SQL Server. This is the recommended next b
|
|||||||
|
|
||||||
Both CI-simulatable on the shared docker host, no hardware.
|
Both CI-simulatable on the shared docker host, no hardware.
|
||||||
|
|
||||||
### MTConnect Agent — 📝 Plan ready
|
### MTConnect Agent — ✅ Done (P1 Agent MVP)
|
||||||
- **Design:** [`2026-07-15-mtconnect-driver-design.md`](2026-07-15-mtconnect-driver-design.md)
|
- **Design:** [`2026-07-15-mtconnect-driver-design.md`](2026-07-15-mtconnect-driver-design.md)
|
||||||
- **Implementation plan:** [`2026-07-24-mtconnect-driver.md`](2026-07-24-mtconnect-driver.md) · [`.tasks.json`](2026-07-24-mtconnect-driver.md.tasks.json) — **23 tasks**. Task 0 is the TrakHound-vs-hand-rolled client decision; browse-picker live-verify is gated on Wave-0 #468.
|
- **Implementation plan:** [`2026-07-24-mtconnect-driver.md`](2026-07-24-mtconnect-driver.md) · [`.tasks.json`](2026-07-24-mtconnect-driver.md.tasks.json) — **23 tasks; 22 built, Task 21 (live `/run` gate) tracked separately in the plan file.**
|
||||||
- **Scope:** P1 Agent MVP (`IDriver`+`ITagDiscovery`+`IReadable`+`ISubscribable`+probe+rediscover),
|
- **Driver guide:** [`docs/drivers/MTConnect.md`](../drivers/MTConnect.md) — config keys, capability
|
||||||
browse **free via the Wave-0 universal browser** (`SupportsOnlineDiscovery=true`, no browser code),
|
surface, data-plane precedence rules, quality-code mapping, and (most load-bearing) the **Known
|
||||||
typed editor, `UNAVAILABLE→BadNoCommunication` mapping, ring-buffer re-baseline paging.
|
limitations** section.
|
||||||
- **Fixture:** canned XML unit fixtures (bulk of coverage) + a dockerized `mtconnect/cppagent`
|
- **Scope shipped:** `IDriver`+`ITagDiscovery`+`IReadable`+`ISubscribable`+`IHostConnectivityProbe`+`IRediscoverable`
|
||||||
integration fixture (env-gated). Depends on Wave 0's browse seam being live-verified (#468).
|
(deliberately no `IWritable` — read-only Agent surface), browse **free via the Wave-0 universal
|
||||||
- **Effort:** S–M (≈1–1.5 wk with TrakHound, ≈2.5–3 wk hand-rolled).
|
browser** (`SupportsOnlineDiscovery=true`, no browser project), typed AdminUI tag editor,
|
||||||
|
`UNAVAILABLE→BadNoCommunication` mapping, CONDITION-as-`String`, ring-buffer re-baseline paging
|
||||||
|
(both the sequence-gap and `OUT_OF_RANGE` legs).
|
||||||
|
- **Diverged from the design:** Task 0/6/7 dropped the planned TrakHound `MTConnect.NET-Common`/`-HTTP`
|
||||||
|
dependency entirely — the pinned packages ship no XML formatter and the HTTP stream client has no
|
||||||
|
injectable handler to test behind the driver's seam. Parsing is hand-rolled `System.Xml.Linq`,
|
||||||
|
matching on `LocalName` only (namespace-agnostic, so 1.3–2.x all parse). See the driver guide's
|
||||||
|
"Built vs. planned" section.
|
||||||
|
- **Test results:** MTConnect unit suite 491/491; integration suite 12/12 against a real Agent
|
||||||
|
(`mtconnect/agent:2.7.0.12`, **not** `mtconnect/cppagent` — that Docker Hub repo doesn't exist);
|
||||||
|
skips cleanly (12 Skipped) offline. AdminUI 749/749; full solution 0 build errors.
|
||||||
|
- **Deferred (documented, not built):** write-back via MTConnect Interfaces (design §3.6); P1.5
|
||||||
|
CONDITION→native Part-9 alarms, `TIME_SERIES` array materialization, EVENT→enum vocab (design §9);
|
||||||
|
P2 SHDR adapter ingest (design §9). Also two pre-existing fleet-wide gaps this build surfaced
|
||||||
|
rather than caused: `IRediscoverable`/`IHostConnectivityProbe` have no server-side consumer (eight
|
||||||
|
drivers besides Galaxy), and no driver form blocks Save on a required-field validation error.
|
||||||
|
- **Fixture:** canned XML unit fixtures (bulk of coverage) + a dockerized `mtconnect/agent` +
|
||||||
|
stdlib-SHDR-adapter integration fixture (env-gated), deployed at `/opt/otopcua-mtconnect/` on the
|
||||||
|
shared docker host. See the fixture's own
|
||||||
|
[`Docker/README.md`](../../tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/Docker/README.md).
|
||||||
|
- **Effort:** S–M, hand-rolled (the TrakHound estimate in the design doc did not apply once that
|
||||||
|
path was ruled out).
|
||||||
|
|
||||||
### MQTT / Sparkplug B — ✅ **COMPLETE** (P1 + P2, both live-gated)
|
### MQTT / Sparkplug B — ✅ **COMPLETE** (P1 + P2, both live-gated)
|
||||||
- **Design:** [`2026-07-15-mqtt-sparkplug-driver-design.md`](2026-07-15-mqtt-sparkplug-driver-design.md)
|
- **Design:** [`2026-07-15-mqtt-sparkplug-driver-design.md`](2026-07-15-mqtt-sparkplug-driver-design.md)
|
||||||
@@ -219,12 +240,12 @@ Both CI-simulatable on the shared docker host, no hardware.
|
|||||||
|
|
||||||
## Next actions
|
## Next actions
|
||||||
|
|
||||||
1. **Close the Wave-0 live gate** (Gitea #468) — unblocks browse verification for MTConnect/BACnet.
|
1. **Close the Wave-0 live gate** (Gitea #468) — unblocks browse verification for BACnet (MTConnect
|
||||||
|
shipped its own Task-21 live `/run` gate independently — see the plan file).
|
||||||
2. **Build Wave 1** — Modbus RTU first (lowest effort, no new infra), then SQL poll. Both plans are
|
2. **Build Wave 1** — Modbus RTU first (lowest effort, no new infra), then SQL poll. Both plans are
|
||||||
📝 ready; run the command from the [Executing a plan](#executing-a-plan-git-worktree--subagent-per-task)
|
📝 ready; run the command from the [Executing a plan](#executing-a-plan-git-worktree--subagent-per-task)
|
||||||
table (subagent-driven-development in a git worktree).
|
table (subagent-driven-development in a git worktree).
|
||||||
3. **Build Wave 2** — MTConnect's browse leg wants #468 closed first. **MQTT / Sparkplug B is
|
3. **Wave 2 is done.** Both deliverables shipped and live-gated: **MQTT / Sparkplug B** (P1 + P2) and **MTConnect Agent** (P1 Agent MVP, PR #506). Remaining work on both is optional follow-on — MQTT P3 write-through (`IWritable`: NCMD/DCMD + plain publish) plus its two browse-UI
|
||||||
COMPLETE** (P1 + P2, both live-gated); its only remaining work is optional — P3 write-through
|
gaps, and MTConnect write-back (deliberately deferred; the Agent surface is read-only).
|
||||||
(`IWritable`: NCMD/DCMD + plain publish) and the two browse-UI gaps recorded above.
|
|
||||||
|
|
||||||
Update this file's Summary table and per-wave status whenever a deliverable changes state.
|
Update this file's Summary table and per-wave status whenever a deliverable changes state.
|
||||||
|
|||||||
-2
@@ -5,8 +5,6 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
|
||||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Pure records + the MTConnect data-type inference table, shared by the driver, browse-commit,
|
<!-- Pure records + the MTConnect data-type inference table, shared by the driver, browse-commit,
|
||||||
|
|||||||
Reference in New Issue
Block a user