Files
lmxopcua/docs/plans/2026-08-05-mtconnect-writeback-design.md
T

163 lines
11 KiB
Markdown
Raw 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 write-back + MQTT write-through record reconciliation — design
**Status:** DONE 2026-08-05 — merged to master (`0e3381d5`); unit suites green (MTConnect 534/534,
MQTT 658/658, AdminUI 1064/1064); **live gate PASSED** (§4): the full integration suite ran live
against the shared fixture (`10.100.0.35:5000`, AllowPut now on) — 15/15, 0 skipped — covering the
write round-trip (value read back from `/current`), the real agent-refusal → `BadCommunicationError`
mapping (with the no-`deviceName` → `BadNodeIdUnknown` falsifiability control), and the disabled
gate (`BadNotWritable`, agent untouched). The same legs also ran against a local OrbStack fixture
during development with agent-log corroboration. **One leg is deliberately deferred to the
operator's chrome session:** the docker-dev rig OPC UA leg (author an MTConnect driver with
`write.enabled` + a `ReadWrite` tag in AdminUI, deploy, `Client.CLI write`, read back) — authoring
requires the AdminUI, and per the operator's instruction chrome-driven gates run separately.
Remember the CurrentWrite caveat when running it: the tag must be authored `ReadWrite`
(browse-commit defaults to `Read`).
## 1. Scope correction: the MQTT half of this request is already shipped
The request ("MQTT P3 write-through plus its two documented browse-UI gaps, MTConnect write-back")
came from `docs/plans/2026-07-24-driver-expansion-tracking.md` §Next actions (lines 288–289) — which
is **stale**. On master today:
| Item | Status | Evidence |
|---|---|---|
| MQTT P3 write-through (`IWritable`: plain `writeTopic` publish + Sparkplug NCMD/DCMD) | **Shipped, live-gated** — #508, `72be1922`, 2026-07-31 | `MqttDriver.cs:65-67` implements `IWritable`; `Mqtt.md:16-20` status banner |
| Browse-UI gap 1 (empty tree cannot arm a rebirth) | **Fixed** — #514, `7f53bd96` | `RawBrowseModal.razor:127-157` manual `{group}/{edgeNode}` scope input |
| Browse-UI gap 2 (slashed metric name refused at browse-commit) | **Fixed** — #509, `7f53bd96` | `RawBrowseCommitMapper.ToRawSegment` (`:107`), collision refusal (`:117`) |
So the only MQTT work left is **documentation reconciliation** — three stale records that now
contradict the code and `Mqtt.md`'s own closure line (`:597-602`):
1. `docs/drivers/Mqtt.md:550-556` — still prints the #514 rebirth gap as an open "Known gap".
2. `docs/drivers/Mqtt.md:586-587` — "Read-only. No writes reach the broker; every node is ViewOnly"
contradicts §Write-through (`:407-480`) in the same file.
3. `docs/plans/2026-07-24-driver-expansion-tracking.md:288-289` §Next actions still lists MQTT P3 +
the two gaps as remaining work; the P2 section (`:239-263`) still describes the gaps as
"documented not fixed".
4. `deferment.md:132-133` — "`IWritable` is absent from MTConnect, MQTT, Sql and Calculation" was
audited pre-#508 and is wrong about MQTT (and, after this branch, about MTConnect).
## 2. MTConnect write-back — the real work
### 2.1 What was deferred, and what mechanism to build on
The driver shipped deliberately read-only (`MTConnectDriver.cs:63` — no `IWritable`;
`docs/plans/2026-07-15-mtconnect-driver-design.md` §3.6). The deferral named **MTConnect
Interfaces** (a request/response state-machine handshake between two agents) as the only write-back
shape and judged it misleading to model as `IWritable` — "revisit only on a concrete deployment
need". This request is that need, but Interfaces is still the wrong mechanism: it is rarely
deployed, requires adapter-side cooperation, and is a state machine rather than a value write.
The mechanism this design uses instead is the **Agent's HTTP PUT/POST surface** — a standard
cppagent feature, off by default, enabled by `AllowPut = true` (optionally scoped by
`AllowPutFrom = <host list>`) in `agent.cfg`. With it enabled, `PUT`/`POST
{agent}/{deviceName}?{dataItemId}={value}` sets the named observation in the agent's buffer exactly
as if an adapter had reported it. The repo's own integration fixture already carries the key —
currently `AllowPut = false` with a "must not accept writes" comment
(`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/Docker/agent.cfg:25-26`).
### 2.2 Honest semantics — this is observation injection, not machine command
A PUT write updates **what the agent reports**, not the machine: the value lands in the agent's
observation buffer, is visible to every agent consumer, and the next adapter/SHDR update for that
data item overwrites it. That is genuinely useful (operator-set EVENT values, external systems
feeding data items the adapter does not own, test rigs) but it must never be documented as
commanding the CNC. The docs section this branch writes states this prominently, and the feature is
**opt-in at two independent layers**: the agent (`AllowPut`) and the driver config
(`write.enabled`, default `false`). Either alone leaves today's read-only behaviour untouched.
### 2.3 Driver design
**Config.** `MTConnectDriverConfigDto` (+ typed `MTConnectDriverOptions`) gains a `Write` block:
```json
"write": { "enabled": true }
```
Default absent/`false` — nothing changes for existing deployments. No per-tag key: writability is
derived (below), matching how the driver derives everything else from the probe model.
**Capability.** `MTConnectDriver` adds `IWritable`. The factory already returns the concrete type
(`MTConnectDriverFactoryExtensions.cs:33-39` documents why), so capability dispatch picks it up
with no registry change.
**`WriteAsync` shape** (mirrors `MqttDriver.cs:670-745` — never throws, batch shape preserved,
per-slot codes):
- `write.enabled == false` → every slot `BadNotWritable` (0x803B0000).
- No live client (pre-Initialize / post-Shutdown) → `BadNotConnected`, matching `ReadAsync`'s
convention (`MTConnectDriver.cs:628-695`).
- RawPath does not resolve via `TagBinding.ToDataItemId` → `BadNodeIdUnknown`.
- Data item is CONDITION-category, or the value is null / not representable as an invariant string
→ `BadNotWritable` / `BadTypeMismatch` respectively. (Condition PUTs use a different level|text
wire format and carry alarm semantics — excluded from v1.)
- HTTP call: `POST {agentBase}/{deviceName}?{dataItemId}={value}` via the agent-client seam. The
owning `deviceName` comes from a `dataItemId → deviceName` map built at bind time from the cached
probe model (falling back to `options.DeviceName`); a multi-device agent is addressed correctly
without config.
- Agent refusal (non-2xx, or an `MTConnectError` document in a 200 body) → `BadCommunicationError`
with the agent's own error text logged (never in `LastError` — type-only discipline).
- `OperationCanceledException` fills remaining slots and returns; any other exception → log warning
+ `BadCommunicationError`.
- Values format with `CultureInfo.InvariantCulture`; booleans as the raw invariant string.
**Seam extension.** `IMTConnectAgentClient` gains
`Task<MTConnectWriteOutcome> WriteObservationAsync(string deviceName, string dataItemId, string value, CancellationToken ct)`
(outcome = accepted / agent-error-with-text / transport-error). `MTConnectAgentClient` implements
it over `_http` (the unary client with the bounded timeout) using `HttpMethod.Post`; the stub
`HttpMessageHandler` ctor seam (`MTConnectAgentClient.cs:107`) covers response-shape unit tests
with no socket. The unit-test `CannedAgentClient` records write calls and returns scripted
outcomes.
**Discovery stamping.** `DiscoverAsync` currently stamps every leaf
`SecurityClassification.ViewOnly` (`MTConnectDriver.cs:1040-1042`) with a comment saying that is
the only honest tier for a driver with no `IWritable`. That derivation becomes conditional:
`CanWrite(dataItem)` (= `write.enabled` && category is not CONDITION) → `Operate`, else `ViewOnly`
— the same "a node cannot advertise writability the driver would refuse" principle MQTT pinned
(`MqttDriver.cs:504`, `CanWrite` at `:625-627`). The implementer must verify how materialization
derives the `CurrentWrite` access bit (capability presence + SecurityClass) so a stamped-Operate
node is actually writable over OPC UA, and a ViewOnly node is not.
**Doc comments to update in the same change:** the class remarks
(`MTConnectDriver.cs:46-53` "deliberately no IWritable"), the factory remark
(`MTConnectDriverFactoryExtensions.cs:39`), and `MTConnectTagDefinition.cs:10-14` ("no
corresponding write path").
### 2.4 Out of scope
- **MTConnect Interfaces** — still deferred, same rationale as §3.6; this design does not touch it.
- Asset writes (`POST /asset`), per-tag write opt-outs, write retry (`isIdempotent` stays `false`
at `DriverInstanceActor.cs:776`), and PUT `time=` timestamps — YAGNI.
- Any AdminUI surface — the driver form gains nothing; `write.enabled` is authored in the existing
driver-config JSON. (No chrome gate needed for this branch.)
### 2.5 Testing
1. **Unit (offline, falsifiable).** New `MTConnectWriteTests`: batch contract + every refusal leg
via `CannedAgentClient` (disabled-by-default, unknown RawPath, condition category, null value,
no client, cancellation fill, agent-error outcome). Encoder-free — the write is a URL, so the
URL/verb/device-resolution assertions live in `MTConnectAgentClient` stub-handler tests
(MTConnectError-in-200 detection included). Discovery stamping test: same probe fixture browsed
with write on/off flips leaf SecurityClass, CONDITION stays ViewOnly.
2. **Integration (env-gated, docker fixture).** Flip the fixture to `AllowPut = true` (update the
comment to say writes are now in scope) and add a write leg: write via the driver → read
`/current` back → the observation carries the written value; a second leg proves an
`AllowPut = false`-style refusal maps to `BadCommunicationError` is *not* needed (we cannot run
two agent configs in one fixture) — instead a bad-dataItemId write asserts the agent-error
mapping. Remove the "(b) Writes" not-covered remark
(`MTConnectAgentIntegrationTests.cs:64-65`).
3. **Live gate (no chrome).** Against the docker-dev rig + the shared fixture agent: author an
MTConnect driver with `write.enabled`, deploy, `Client.CLI write` to a writable node, confirm
via `/current` and via OPC UA read-back. Runnable headless by the executing session.
## 3. Bookkeeping (both halves)
- `docs/drivers/MTConnect.md`: capability surface (`:71-77`), Deferred (`:295-301`), new
§Write-back section with the §2.2 semantics warning.
- `docs/plans/2026-07-15-mtconnect-driver-design.md` §3.6: superseded note pointing here.
- `docs/plans/2026-07-24-driver-expansion-tracking.md`: MQTT P2 gap bullets + §Next actions
rewritten to record #508/#509/#514 closure and this branch's MTConnect write-back.
- `docs/drivers/Mqtt.md`: delete stale blocks `:550-556` and `:586-587`.
- `deferment.md`: correct `:132-133` (post-branch: `IWritable` absent from Sql and Calculation
only) and the MTConnect row (`:126`).