docs: MTConnect write-back recorded; stale MQTT write-through/browse-gap records reconciled

This commit is contained in:
Joseph Doherty
2026-08-05 06:55:14 -04:00
parent b833dda673
commit 203000a80a
5 changed files with 270 additions and 62 deletions
@@ -114,6 +114,21 @@ Pure function in `.Contracts` (shared by driver, browser-commit, and the typed e
### 3.6 `IWritable` — **not implemented (v1)**
> **SUPERSEDED 2026-08-05 — write-back shipped, by a different mechanism.**
> `MTConnectDriver` now implements `IWritable` (`b29623cf`) over the Agent's optional
> **`AllowPut`** surface (`POST {agent}/{device}?{dataItemId}={value}`), gated off by
> default at both the Agent (`AllowPut`) and the driver (`Write.Enabled`). That is
> **observation injection into the Agent's buffer, not machine command** — see
> [`2026-08-05-mtconnect-writeback-design.md`](2026-08-05-mtconnect-writeback-design.md)
> §2.2 and `docs/drivers/MTConnect.md` §Write-back.
> **The paragraph below is retained because its rationale still holds for *Interfaces*,
> which remains deferred** (design §2.1 of the write-back doc re-affirms it: rarely
> deployed, needs adapter-side cooperation, a state machine rather than a value write).
> Only its two incidental claims about this build are out of date — the driver is no
> longer `IWritable`-less, and nodes no longer materialize without `CurrentWrite` *because
> of a missing capability*; the `CurrentWrite` bit was in fact always composed from the
> authored `Tag.AccessLevel` alone, never from driver capability.
Justified per research §2.1: the MTConnect Agent surface is read-only by design; write-back exists only via optional, rarely-deployed MTConnect *Interfaces* (a request/response state-machine handshake, not a "set value" — modelling it as `IWritable` would mislead). Capability interfaces are composable, so omitting `IWritable` is idiomatic (Galaxy's write path is even fire-and-forget). Nodes materialize **without** the `AccessLevels.CurrentWrite` bit automatically. Revisit Interfaces only if a concrete deployment needs it.
### 3.7 `IHostConnectivityProbe` + `IRediscoverable`
@@ -179,7 +179,8 @@ Both CI-simulatable on the shared docker host, no hardware.
surface, data-plane precedence rules, quality-code mapping, and (most load-bearing) the **Known
limitations** section.
- **Scope shipped:** `IDriver`+`ITagDiscovery`+`IReadable`+`ISubscribable`+`IHostConnectivityProbe`+`IRediscoverable`
(deliberately no `IWritable` — read-only Agent surface), browse **free via the Wave-0 universal
(P1 shipped deliberately without `IWritable`; **`IWritable` added 2026-08-05, `b29623cf`** — see the
Deferred line below), browse **free via the Wave-0 universal
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).
@@ -191,7 +192,18 @@ Both CI-simulatable on the shared docker host, no hardware.
- **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
- **Write-back — SHIPPED 2026-08-05, not via Interfaces** (`b29623cf`, branch
`feature/mqtt-mtconnect-write`; design
[`2026-08-05-mtconnect-writeback-design.md`](2026-08-05-mtconnect-writeback-design.md)).
`IWritable` over the Agent's optional **`AllowPut`** surface
(`POST {agent}/{device}?{dataItemId}={value}`) — **observation injection into the Agent's
buffer, never a machine command**, gated off by default at both the Agent (`AllowPut`) and the
driver (`Write.Enabled`), CONDITION items refused. **MTConnect *Interfaces* remains deferred**
— design §3.6's rationale still holds for it (rare, adapter-side cooperation, a state machine
rather than a value write), and it is a different mechanism, not this one. Caveat recorded in
the driver guide: the OPC UA `CurrentWrite` bit still comes from the authored
`Tag.AccessLevel` alone, so an operator must author **ReadWrite**.
- **Deferred (documented, not built):** MTConnect *Interfaces* (design §3.6, above); 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
@@ -210,7 +222,11 @@ Both CI-simulatable on the shared docker host, no hardware.
`OnDataChange`, retained last-value read, `#`-observation browser); P2 Sparkplug B ingest
(vendored Tahu proto + `Grpc.Tools` codegen, birth/alias/seq-gap/rebirth state machine,
death→STALE, birth-driven browse tree, scoped Request-rebirth NCMD, typed tag editor).
**Write-through (`IWritable`) is deferred to P3** — every MQTT node materializes read-only.
**P3 write-through (`IWritable`) shipped 2026-07-31** — `72be1922`
([#508](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/508), live-gated): plain
`writeTopic` publish + Sparkplug NCMD/DCMD, with a node discovered `ViewOnly` when the encoder
would refuse the write. Superseded the "every MQTT node materializes read-only" line this entry
carried; see `docs/drivers/Mqtt.md` §Write-through.
- **Fixture:** Mosquitto TLS+auth broker + JSON publisher sidecar, **live** at `10.100.0.35:8883`
(`:1883` plaintext-but-authenticated); stack `/opt/otopcua-mqtt`, compose in
`tests/Drivers/…​.Driver.Mqtt.IntegrationTests/Docker/`. Env-gated live suite
@@ -236,13 +252,31 @@ Both CI-simulatable on the shared docker host, no hardware.
Sparkplug keys, merge-not-replace on save, group-id segment validation) + 8 falsifiable
`MqttDriverFormModelTests` cases. **Three gates, three instances of the same defect class: a
hand-maintained AdminUI surface left behind by a driver-side feature.**
- **P2 live gate — two open UI gaps, documented not fixed** (see `docs/drivers/Mqtt.md` §Known gaps):
- **A completely empty browse tree cannot arm a rebirth.** The scope comes from a tree click, so on
a plant that has not birthed since the window opened `Request rebirth…` stays disabled — the very
case the affordance exists for. `MqttBrowseSession.ResolveRebirthTargets` already accepts a bare
`{group}/{edgeNode}` ("the prime rebirth target"); only a UI path to enter one is missing.
Mitigation shipped: a **Refresh** button on the browse tree (it re-reads the same session, which
previously rendered exactly once at open and never again).
- **P2 live gate — four UI gaps recorded, ALL FOUR NOW CLOSED 2026-07-31** (`7f53bd96`; see
`docs/drivers/Mqtt.md` §Known gaps, which lists the closures). Recorded as found, with what
actually fixed each:
- **A completely empty browse tree cannot arm a rebirth**
([#514](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/514)). The scope comes from a tree
click, so on a plant that has not birthed since the window opened `Request rebirth…` stays
disabled — the very case the affordance exists for. `MqttBrowseSession.ResolveRebirthTargets`
already accepts a bare `{group}/{edgeNode}` ("the prime rebirth target"); only a UI path to
enter one was missing. Mitigation shipped at the time: a **Refresh** button on the browse tree
(it re-reads the same session, which previously rendered exactly once at open and never again).
**FIXED —** `RawBrowseModal` now offers a `group/edgeNode` scope input whenever no tree scope is
armed; edge-node only, never a typed group broadcast.
- **A metric name containing `/` cannot be browse-committed**
([#509](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/509)). The derived raw **tag Name**
is a RawPath segment and may not contain `/`, while `metricName` legitimately may — so the
spec-mandatory `Node Control/Rebirth` was refused at commit (`Row N: Name must not contain '/'`).
The refusal was loud and all-or-nothing, never a silently mis-bound tag, and **Manual entry** was
the working path. Fixing it needed a name-sanitisation policy with a collision answer, so it was
recorded rather than guessed at. **FIXED —** `RawBrowseCommitMapper.ToRawSegment` sanitises the
segment and refuses a collision explicitly.
- **`_canRebirth` is captured at browse-open**, so a reaped session still drew the button
([#510](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/510)). **FIXED.**
- **The tag editor injects the Plain-only `payloadFormat` key into a Sparkplug blob**
([#513](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/513)) — cosmetic; the factory
routes on the Sparkplug tuple and ignored it. **FIXED.**
- **P2 live gate also found a PRE-EXISTING, cross-driver AdminUI defect — FIXED:** every node label in
the shared `DriverBrowseTree` was an `<a href="#">`. In a Blazor Web App, `blazor.web.js`'s
enhanced-navigation click interceptor resolves a bare `#` against `<base href="/">`, so **clicking
@@ -252,19 +286,14 @@ Both CI-simulatable on the shared docker host, no hardware.
now `<button type="button">`. It dates to the component's introduction (2026-05-28) and affects
**every** driver's picker; it only surfaced now because Sparkplug's Request-rebirth is the first
feature that requires clicking a *label* rather than the ▶ toggle (always a `<button>`, always fine).
- **A metric name containing `/` cannot be browse-committed.** The derived raw **tag Name** is a
RawPath segment and may not contain `/`, while `metricName` legitimately may — so the
spec-mandatory `Node Control/Rebirth` is refused at commit (`Row N: Name must not contain '/'`).
The refusal is loud and all-or-nothing, never a silently mis-bound tag, and **Manual entry** is
the working path (the Sparkplug tag editor accepts a slashed metric name). Fixing it needs a
name-sanitisation policy with a collision answer, so it was recorded rather than guessed at.
- `_canRebirth` is captured at browse-open, so a reaped session still draws the button.
- The tag editor injects the Plain-only `payloadFormat` key into a Sparkplug blob — cosmetic; the
factory routes on the Sparkplug tuple and ignores it.
- **Rediscovery is inert in v3 (platform gap, not MQTT's).** The Sparkplug driver raises
`OnRediscoveryNeeded` on a changed birth metric-set, but **nothing subscribes to it** and
`DriverHostActor.HandleDiscoveredNodes` hard-returns. A DBIRTH introducing a metric does **not**
change the OPC UA tree — redeploy. Driver-side behaviour is log-observable only.
- **Rediscovery was inert in v3 (platform gap, not MQTT's) — HALF-CLOSED since 2026-07-27.** The
Sparkplug driver raises `OnRediscoveryNeeded` on a changed birth metric-set; at the time nothing
subscribed to it and `DriverHostActor.HandleDiscoveredNodes` hard-returned. `DriverInstanceActor`
now **consumes** the raise (`09a401b8`) and it rides the driver-health snapshot to the AdminUI
`/hosts` page as a **re-browse chip**, while the injection path was **deleted** rather than
revived (#507, `adce27e7` — its two inputs are structurally empty in v3). So the standing part is
unchanged and deliberate: a DBIRTH introducing a metric still does **not** change the served OPC
UA tree; an operator re-browses and commits through `/raw`.
- **Redundant-pair hazard (documented, not a code change):** both nodes of a pair run the driver, so a
**fixed `clientId` makes them evict each other forever** — the broker logs
`already connected, closing old connection` and both nodes reconnect every ~2 s while still
@@ -285,7 +314,24 @@ Both CI-simulatable on the shared docker host, no hardware.
`0f38f486` / #495) and **SQL poll** (merged `4ad54037`; follow-ups #496/#497/#498 closed in
`28c28667`). Remaining SQL work is optional follow-on — Postgres/ODBC dialects behind
`ISqlDialect`, and `SqlTagModel.Query` (P3, currently rejected end-to-end by design).
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
gaps, and MTConnect write-back (deliberately deferred; the Agent surface is read-only).
3. **Wave 2 is done, and so is the follow-on this entry used to list.** Both deliverables shipped and
live-gated: **MQTT / Sparkplug B** (P1 + P2) and **MTConnect Agent** (P1 Agent MVP, PR #506).
Since then:
- **MQTT P3 write-through shipped and was live-gated 2026-07-31** — `72be1922`
([#508](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/508)): plain `writeTopic` publish
+ Sparkplug NCMD/DCMD.
- **Both browse-UI gaps are closed**, with the other two P2 records, in `7f53bd96`
([#509](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/509),
[#510](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/510),
[#513](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/513),
[#514](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/514)) — see the P2 section above.
- **MTConnect write-back is landing on `feature/mqtt-mtconnect-write`** (`b29623cf`), by
**observation injection** through the Agent's `AllowPut` surface rather than the Interfaces
handshake the original deferral assumed — design
[`2026-08-05-mtconnect-writeback-design.md`](2026-08-05-mtconnect-writeback-design.md), which
is also the record for this reconciliation. MTConnect *Interfaces* stays deferred.
Remaining optional follow-on for the pair: MTConnect P1.5 (CONDITION→Part-9 alarms,
`TIME_SERIES` arrays, EVENT→enum vocab) and P2 SHDR ingest.
Update this file's Summary table and per-wave status whenever a deliverable changes state.