From a0b5095b3bf548a4746ea5b9f86cda2cff8a2908 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 27 Jul 2026 13:52:08 -0400 Subject: [PATCH] docs(drivers): un-stale the MQTT README rows + track every known gap as an issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cross-driver README still described MQTT as pre-P2 — "Sparkplug B is P2 (`mode: SparkplugB` is a placeholder)" and "Sparkplug B has no fixture yet (P2)". Both were false as of the P2 merge (c3a2b0f7): SparkplugB is a fully implemented mode and the live suite is 15 tests (7 plain + 8 Sparkplug) against a project-owned C# edge-node simulator. Mqtt.md and the scadaproj index were both already correct; only this file missed the P2 sweep. Filed the nine Known-gaps rows as tracking issues and linked them, so the table stops being the only record of them: #507 IRediscoverable is inert in v3 (platform gap — also Galaxy/TwinCAT/AbCip) #508 write-through (IWritable) — NCMD/DCMD + plain publish #509 a metric name containing '/' cannot be browse-committed #510 _canRebirth captured at browse-open — reaped session still draws button #511 Sparkplug primary-host STATE publishing (ActAsPrimaryHost) #512 .Browser references .Driver — extract the client-options builder #513 editor writes a meaningless payloadFormat into a Sparkplug blob #514 Request-rebirth unarmable on a plant that birthed before the window #515 DataSet / Template / PropertySet metric support #507 is the one worth reading: it is a v3 platform gap, not an MQTT bug — DriverHostActor.HandleDiscoveredNodes hard-returns, so every IRediscoverable driver fires into a void and the edge gaining a point needs a redeploy to show up. No code change; docs + issue links only. --- docs/drivers/Mqtt.md | 33 +++++++++++++++++++-------------- docs/drivers/README.md | 6 +++--- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/docs/drivers/Mqtt.md b/docs/drivers/Mqtt.md index dc3806a9..25c54975 100644 --- a/docs/drivers/Mqtt.md +++ b/docs/drivers/Mqtt.md @@ -14,8 +14,9 @@ for the design. > **Status.** P1 (plain MQTT) and P2 (Sparkplug B ingest) are both shipped and live-gated against the > Mosquitto + Sparkplug-simulator fixture. Write-through (`IWritable` — NCMD/DCMD/plain publish) is -> **deferred**; every MQTT node materializes read-only. See -> [Known gaps](#known-gaps) for what is wired but not yet end-to-end observable. +> **deferred** ([#508](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/508)); every MQTT node +> materializes read-only. See [Known gaps](#known-gaps) — every entry there carries a tracking issue +> (#507–#515). ## Project Layout @@ -28,8 +29,9 @@ for the design. > `.Browser` references `.Driver` (not just `.Contracts`) — a **documented, deliberate exception** > so browse can reuse `MqttConnection.BuildClientOptions` (TLS / CA-pin / credentials / protocol > mapping). Its cost is pulling `Core` into the AdminUI graph. The csproj carries a boxed warning; -> do not copy the pattern into a new driver's browser. The clean fix (deferred) is a leaf -> `Driver.Mqtt.Transport` project. +> do not copy the pattern into a new driver's browser. The clean fix — extract the pure +> options builder down into `.Contracts` — is tracked as +> [#512](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/512). ## Capability Surface @@ -380,13 +382,16 @@ identical words. See [TestConnectProbes.md](TestConnectProbes.md). ## Known gaps -| Gap | Effect | Notes | -|---|---|---| -| **Write-through (`IWritable`)** | Every MQTT node is read-only | Deferred to P3 — NCMD/DCMD + plain publish, optimistic-Good + self-correct on echo | -| **Rediscovery is inert** | A DBIRTH introducing a metric does not change the OPC UA tree; redeploy to pick it up | v3 platform gap — nothing subscribes to `OnRediscoveryNeeded` and `DriverHostActor.HandleDiscoveredNodes` hard-returns. Driver-side decision is log-observable | -| **Rebirth needs a non-empty tree** | `Request rebirth…` cannot be armed on a plant that has not birthed since the window opened | See [Refresh](#refresh). Session side already supports a bare `{group}/{edgeNode}` scope | -| **`_canRebirth` is captured at browse-open** | A session reaped for idleness (2 min) still draws the button, and using it errors | The error is shown, not swallowed; reopen the browser | -| **Primary-host STATE publishing** | `ActAsPrimaryHost` does nothing | Logs a startup warning rather than being silently inert — see the `Sparkplug` sub-object table | -| **A metric whose name contains `/` cannot be browse-committed** | The commit is refused **whole**, in words (`Row N: Name must not contain '/'`) | The derived **tag Name** is a RawPath segment and may not contain `/`, while `metricName` legitimately may. This blocks the spec-mandatory `Node Control/Rebirth`. **Workaround: Manual entry** — author the tag with a `/`-free name and type the slashed `metricName` into the Sparkplug editor, which accepts it. Nothing is silently mis-bound | -| **`DataSet` / `Template` / `PropertySet` metrics** | Skipped with a warning | v1-unsupported; never coerced into a guessed type | -| **The tag editor writes `payloadFormat` into a Sparkplug blob** | Cosmetic only | `payloadFormat` is Plain-only; the factory routes on the Sparkplug keys and ignores it. Noise in the blob, not a mis-binding | +Every row is tracked. None is a silent failure — each either refuses in words or logs. + +| Gap | Issue | Effect | Notes | +|---|:---:|---|---| +| **Write-through (`IWritable`)** | [#508](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/508) | Every MQTT node is read-only | Deferred to P3 — NCMD/DCMD + plain publish, optimistic-Good + self-correct on echo | +| **Rediscovery is inert** | [#507](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/507) | A DBIRTH introducing a metric does not change the OPC UA tree; redeploy to pick it up | v3 **platform** gap, not MQTT-specific — nothing subscribes to `OnRediscoveryNeeded` and `DriverHostActor.HandleDiscoveredNodes` hard-returns. Also affects Galaxy / TwinCAT / AbCip. Driver-side decision is log-observable | +| **Rebirth needs a non-empty tree** | [#514](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/514) | `Request rebirth…` cannot be armed on a plant that has not birthed since the window opened | See [Refresh](#refresh). Session side already supports a bare `{group}/{edgeNode}` scope | +| **`_canRebirth` is captured at browse-open** | [#510](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/510) | A session reaped for idleness (2 min) still draws the button, and using it errors | The error is shown, not swallowed; reopen the browser | +| **Primary-host STATE publishing** | [#511](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/511) | `ActAsPrimaryHost` does nothing | Logs a startup warning rather than being silently inert — see the `Sparkplug` sub-object table | +| **A metric whose name contains `/` cannot be browse-committed** | [#509](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/509) | The commit is refused **whole**, in words (`Row N: Name must not contain '/'`) | The derived **tag Name** is a RawPath segment and may not contain `/`, while `metricName` legitimately may. This blocks the spec-mandatory `Node Control/Rebirth`. **Workaround: Manual entry** — author the tag with a `/`-free name and type the slashed `metricName` into the Sparkplug editor, which accepts it. Nothing is silently mis-bound | +| **`DataSet` / `Template` / `PropertySet` metrics** | [#515](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/515) | Skipped with a warning | v1-unsupported; never coerced into a guessed type | +| **The tag editor writes `payloadFormat` into a Sparkplug blob** | [#513](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/513) | Cosmetic only | `payloadFormat` is Plain-only; the factory routes on the Sparkplug keys and ignores it. Noise in the blob, not a mis-binding | +| **`.Browser` references `.Driver`, not just `.Contracts`** | [#512](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/512) | A documented layering exception | Taken to reuse `MqttConnection.BuildClientOptions` rather than duplicate TLS/CA-pin logic — see the note at the top of this doc | diff --git a/docs/drivers/README.md b/docs/drivers/README.md index 32a63e7c..98c86067 100644 --- a/docs/drivers/README.md +++ b/docs/drivers/README.md @@ -29,7 +29,7 @@ Driver type metadata is registered at startup in `DriverTypeRegistry` (`src/Core | [TwinCAT](TwinCAT.md) | `Driver.TwinCAT` | B | Beckhoff `TwinCAT.Ads` (`TcAdsClient`) | IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IRediscoverable | The only native-notification driver outside Galaxy — ADS delivers `ValueChangedCallback` events the driver forwards straight to `ISubscribable.OnDataChange` without polling. Symbol tree uploaded via `SymbolLoaderFactory` | | [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 | -| [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. Sparkplug B is P2 (`mode: SparkplugB` is a placeholder) | +| [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) | | [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 @@ -48,7 +48,7 @@ Driver type metadata is registered at startup in `DriverTypeRegistry` (`src/Core - [S7.md](S7.md) — Siemens S7 driver (S7-300/400/1200/1500 + S7-200): getting started, config, data-block addressing, serialized single-connection model - [TwinCAT.md](TwinCAT.md) — Beckhoff TwinCAT (ADS) driver: getting started, native-notification subscription, symbol-tree upload - [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 + - [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 - **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).) @@ -66,7 +66,7 @@ Each driver has a dedicated fixture doc that lays out what the integration / uni - [TwinCAT](TwinCAT-Test-Fixture.md) — XAR-VM integration scaffolding (task #221); three smoke tests skip when VM unreachable. Unit via `FakeTwinCATClient` with native-notification harness - [FOCAS](FOCAS-Test-Fixture.md) — no integration fixture, unit-only via `FakeFocasClient`; Tier C out-of-process isolation scoped but not shipped - [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 (7 tests) covers TLS/auth connect, CA pinning both ways, subscribe→value, retained seeding, and wrong-password rejection. Sparkplug B has no fixture yet (P2) +- [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) ## Related cross-driver docs