docs: MTConnect write-back recorded; stale MQTT write-through/browse-gap records reconciled
This commit is contained in:
+120
-15
@@ -15,7 +15,7 @@ fixture recipe read
|
||||
|
||||
## What it talks to
|
||||
|
||||
A **MTConnect Agent** — the vendor-neutral read-only telemetry endpoint that
|
||||
A **MTConnect Agent** — the vendor-neutral 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:
|
||||
@@ -25,8 +25,13 @@ three standard REST paths:
|
||||
- `/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.
|
||||
Those three are the whole data plane: **agent-first** — Discover + Read +
|
||||
Subscribe. There is a fourth path, and it is not part of the data plane: the
|
||||
Agent's optional **`AllowPut`** surface, off by default in `agent.cfg`, which
|
||||
this driver's `IWritable` uses (`b29623cf`, 2026-08-05). It sets what the Agent
|
||||
*reports* for a DataItem; nothing reaches the machine. Read
|
||||
[Write-back](#write-back--observation-injection-off-by-default) before enabling
|
||||
it.
|
||||
|
||||
## Built vs. planned — read this before trusting the design doc's §2
|
||||
|
||||
@@ -68,18 +73,22 @@ below).
|
||||
|
||||
## Capability surface
|
||||
|
||||
`MTConnectDriver : IDriver, IReadable, ISubscribable, ITagDiscovery, IHostConnectivityProbe, IRediscoverable`
|
||||
`MTConnectDriver : IDriver, IReadable, IWritable, 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).
|
||||
`IWritable` landed 2026-08-05 (`b29623cf`), against the Agent's `AllowPut`
|
||||
surface rather than MTConnect *Interfaces*. It is **observation injection, not
|
||||
machine command**, and is off at two independent gates by default — read
|
||||
[Write-back](#write-back--observation-injection-off-by-default) before
|
||||
authoring a writable MTConnect tag. *Interfaces* itself — the protocol's own
|
||||
request/response handshake — is a different mechanism and remains deferred; 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 |
|
||||
| `IWritable` | `WriteAsync` → `POST {agent}/{device}?{dataItemId}={value}` on the Agent's `AllowPut` surface | **Off by default** — needs `Write.Enabled` **and** the Agent's `AllowPut`; CONDITION items are refused |
|
||||
| `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) |
|
||||
@@ -105,6 +114,7 @@ correct and tested, just not on the hot path.
|
||||
"HeartbeatMs": 10000,
|
||||
"Probe": { "Enabled": true, "IntervalMs": 5000, "TimeoutMs": 2000 },
|
||||
"Reconnect": { "MinBackoffMs": 0, "MaxBackoffMs": 30000, "BackoffMultiplier": 2.0 },
|
||||
"Write": { "Enabled": false },
|
||||
"Tags": []
|
||||
}
|
||||
}
|
||||
@@ -123,13 +133,14 @@ Read off `MTConnectDriverOptions` / `MTConnectDriverConfigDto`
|
||||
| Key | Default | Notes |
|
||||
|---|---|---|
|
||||
| `AgentUri` | — (required) | Agent base URI; the driver appends `/probe`, `/current`, `/sample` |
|
||||
| `DeviceName` | `null` | Scopes requests to `{AgentUri}/{DeviceName}/...`; `null` = whole Agent |
|
||||
| `DeviceName` | `null` | **Double duty.** (1) Scopes requests to `{AgentUri}/{DeviceName}/...`; `null` = whole Agent. (2) The write path's fallback device — the device a write is addressed to when the probe model does not say which device declared the DataItem (see [Write-back](#write-back--observation-injection-off-by-default)) |
|
||||
| `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 |
|
||||
| `Write.Enabled` | `false` | The driver-side half of the write-back opt-in. An absent block means disabled — see [Write-back](#write-back--observation-injection-off-by-default) |
|
||||
| `Tags[]` | `[]` | Pre-v3 / CLI authoring surface — one `MTConnectTagDefinition` per DataItem `id` |
|
||||
| `RawTags[]` | `[]` (deploy-injected) | The v3 data-plane binding — see below |
|
||||
|
||||
@@ -224,6 +235,84 @@ the two need disambiguating, not just OR-ing together). On a changed
|
||||
`/hosts` page — see [Known limitations](#known-limitations) for what that does
|
||||
and does not do.
|
||||
|
||||
## Write-back — observation injection, off by default
|
||||
|
||||
> ⚠️ **A write sets what the Agent *reports*, not what the machine does.** The value
|
||||
> lands in the Agent's observation buffer exactly as if an adapter had reported it —
|
||||
> visible to **every** consumer of that Agent, and **overwritten by the next
|
||||
> adapter/SHDR update for that DataItem**. Nothing reaches the CNC. Never document,
|
||||
> script, or operate this as a machine command. It is genuinely useful for
|
||||
> operator-set EVENT values, for external systems feeding DataItems the adapter does
|
||||
> not own, and for test rigs — and nothing else.
|
||||
|
||||
The mechanism is the Agent's **`AllowPut`** surface (`POST
|
||||
{agent}/{device}?{dataItemId}={value}`), a standard cppagent feature that is **off by
|
||||
default**. It is not MTConnect *Interfaces* — see [Deferred](#deferred-not-in-this-build)
|
||||
for why Interfaces was rejected as the write-back shape. Design record:
|
||||
[`2026-08-05-mtconnect-writeback-design.md`](../plans/2026-08-05-mtconnect-writeback-design.md)
|
||||
§2.2; implementation `b29623cf`.
|
||||
|
||||
### Two independent gates, both off by default
|
||||
|
||||
1. **The Agent** — `AllowPut = true` in `agent.cfg`. Optionally scope it with
|
||||
`AllowPutFrom = <host list>` so only named hosts may write; that list is the Agent's
|
||||
own access control and this driver neither reads nor influences it.
|
||||
2. **The driver** — `"Write": { "Enabled": true }` in the driver config (default
|
||||
`false`; an absent block is disabled).
|
||||
|
||||
Either gate alone leaves the shipped read-only behaviour unchanged: with `Write.Enabled`
|
||||
off every slot is refused `BadNotWritable` without an Agent being touched, and with
|
||||
`AllowPut` off the Agent refuses and the driver reports `BadCommunicationError`.
|
||||
|
||||
### The OPC UA write bit is authored, not derived — read this before filing a bug
|
||||
|
||||
**Enabling write-back does not make any existing node writable over OPC UA.** The
|
||||
`CurrentWrite` access bit is composed from the **authored `Tag.AccessLevel`** alone
|
||||
(`AddressSpaceComposer` → `AddressSpaceApplier`, which additionally requires a
|
||||
non-array tag → `OtOpcUaNodeManager.ComposeAccessLevel`); driver capability is never
|
||||
consulted at compose time. An operator must set the raw tag to **ReadWrite** — a
|
||||
browse-committed tag is created `Read` (`RawBrowseCommitMapper.MapLeaf`) whatever the
|
||||
browse said.
|
||||
|
||||
Browse *does* stamp writable leaves `SecurityClassification.Operate` when
|
||||
`Write.Enabled` is on (and `ViewOnly` otherwise, CONDITION always `ViewOnly`), on the
|
||||
same "a node cannot advertise writability the driver would refuse" principle MQTT
|
||||
pinned. But that stamp is **honest labelling, not the access bit**: nothing persists
|
||||
`SecurityClass` — `Tag` has no such column — so it reaches the browse session's
|
||||
attribute side-panel and stops there.
|
||||
|
||||
### What a write does, slot by slot
|
||||
|
||||
`WriteAsync` never throws and one bad slot never fails the batch (matching `ReadAsync`);
|
||||
even cancellation returns a full-length result set. The refusal order is fixed, and each
|
||||
step answers a different operator question:
|
||||
|
||||
| Condition | Status | Meaning |
|
||||
|---|---|---|
|
||||
| `Write.Enabled` off, **or** the DataItem is CONDITION-category | `BadNotWritable` (`0x803B0000`) | Nothing was attempted; no Agent was involved |
|
||||
| No live Agent client (pre-`InitializeAsync`, faulted initialize, post-shutdown) | `BadNotConnected` (`0x808A0000`) | This driver is not running |
|
||||
| The reference resolves to no DataItem, or to one whose owning device cannot be named | `BadNodeIdUnknown` (`0x80340000`) | There is no Agent URL the write could be addressed to |
|
||||
| The value has no invariant string form — `null`, or non-scalar | `BadTypeMismatch` (`0x80740000`) | Arrays are refused rather than rendered (`Convert.ToString` would emit the literal `System.Double[]`, which the Agent stores happily) |
|
||||
| The Agent refused, or could not be reached | `BadCommunicationError` (`0x80050000`) | Includes an `MTConnectError` document returned under HTTP 200 |
|
||||
|
||||
**CONDITION DataItems are excluded deliberately** — they carry alarm semantics and a
|
||||
different `level|text` wire format, and are out of scope for v1.
|
||||
|
||||
**The Agent's refusal text is logged at Warning and never stored.** It stays out of
|
||||
`DriverHealth.LastError`, which is type-only by repo-wide discipline, and a refused write
|
||||
is not evidence the read path degraded — so a refusal leaves driver health untouched.
|
||||
|
||||
**Device resolution needs no extra config.** `TagBinding.WriteTargetsByDataItemId`,
|
||||
built at bind time from a walk of the cached probe model, remembers which device declared
|
||||
each DataItem, so a multi-device Agent is addressed correctly; the fallback is the
|
||||
configured `DeviceName`, and an unresolvable device is `BadNodeIdUnknown` rather than a
|
||||
guess.
|
||||
|
||||
**Values are rendered with `CultureInfo.InvariantCulture`.** A Boolean therefore writes
|
||||
the invariant string `"True"`/`"False"`, **not** `"1"`/`"0"` — if the consuming adapter or
|
||||
Agent vocabulary expects something else, author the tag as a `String` and write the exact
|
||||
token.
|
||||
|
||||
## Browse — free via the universal browser
|
||||
|
||||
MTConnect ships **no bespoke browser project**. Setting
|
||||
@@ -272,6 +361,13 @@ for anything beyond values-and-conditions.
|
||||
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.
|
||||
|
||||
**The write path is the one place a device name is resolved per DataItem**,
|
||||
and it does not use `RawTagEntry.DeviceName` either: it reads the owning
|
||||
device out of the cached probe model and falls back to the top-level
|
||||
`DeviceName` (see [Write-back](#write-back--observation-injection-off-by-default)).
|
||||
So `MTConnectDriverOptions.DeviceName` does double duty — request scope for
|
||||
reads/browse, fallback write target for writes.
|
||||
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
|
||||
@@ -294,11 +390,17 @@ for anything beyond values-and-conditions.
|
||||
|
||||
## 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.
|
||||
- **MTConnect *Interfaces*.** Still deferred, and for the reason design
|
||||
[§3.6](../plans/2026-07-15-mtconnect-driver-design.md#36-iwritable--not-implemented-v1)
|
||||
gave: it is a rare, optional request/response **state-machine handshake**
|
||||
requiring adapter-side cooperation, not a "set value" operation, and would
|
||||
mislead if modelled as `IWritable`.
|
||||
**Write-back itself is no longer deferred** — `b29623cf` shipped `IWritable`
|
||||
over the Agent's `AllowPut` observation-injection surface instead, a different
|
||||
mechanism entirely. See [Write-back](#write-back--observation-injection-off-by-default)
|
||||
and
|
||||
[`2026-08-05-mtconnect-writeback-design.md`](../plans/2026-08-05-mtconnect-writeback-design.md)
|
||||
§2.1.
|
||||
- **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
|
||||
@@ -317,7 +419,10 @@ for anything beyond values-and-conditions.
|
||||
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).
|
||||
sequence-gap and the `OUT_OF_RANGE` legs). The write path adds
|
||||
`MTConnectAgentClientWriteTests` (`9f17d7ef` — verb, URL, agent-error-in-200
|
||||
detection) and `MTConnectWriteTests` (`b29623cf` — every refusal leg, the
|
||||
batch shape, the browse-stamping flip).
|
||||
- **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
|
||||
|
||||
Reference in New Issue
Block a user