Files
lmxopcua/docs/plans/2026-07-24-driver-expansion-tracking.md
T

339 lines
28 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.
# Driver-expansion — Waves 0–2 tracking
> **Living status tracker** for the driver-expansion program (Waves 0, 1, 2). One row per
> deliverable, each pointing at its **design doc**, its **implementation plan** (once written),
> and its current **status**. The authoritative *architecture* index is the program design doc;
> this file is the authoritative *progress* index.
>
> **Program design (architecture / shared contract / build order):**
> [`2026-07-15-driver-expansion-program-design.md`](2026-07-15-driver-expansion-program-design.md)
>
> Last updated: 2026-08-05.
## Legend
| Status | Meaning |
|---|---|
| ✅ **Done** | Merged to master, tests green. |
| 🟡 **Live gate open** | Code merged; a live `/run` or hardware-gated verification still outstanding. |
| 📝 **Plan ready** | Executable implementation plan (`*-implementation.md` + `.tasks.json`) written; not yet built. |
| 📐 **Design only** | Design doc exists; **no** implementation plan yet — writing-plans is the next step. |
| ⛔ **Not started** | No design, no plan. |
**Doc types** (per the writing-plans skill): a *design* states architecture, decisions, and risks;
an *implementation plan* is the bite-sized, TDD, file-path-level task list the subagent-driven
executor runs off, with a co-located `.tasks.json` for resume. A deliverable is only buildable once
it reaches 📝.
## Summary
| Wave | Deliverable | Design | Impl. plan | Status | Effort | Fixture / hardware |
|---|---|---|---|---|---|---|
| **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) | ✅ **Done** — merged `056887d6`; live gate (Gitea #468) CLOSED 2026-08-01 via the patched AbCip fixture (`c2c3c522`) | S–M | patched libplctag `ab_server` (serves `@tags`) at `10.100.0.35:44818` |
| **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) | ✅ **Done** — merged to master `0f38f486` (#495), 11 tasks, live gate PASSED | **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) | ✅ **Done** — merged to master `4ad54037`, 22 tasks, live gate PASSED; follow-ups #496/#497/#498 closed in `28c28667` | 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) | ✅ **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` |
> Waves 3+ (BACnet/IP, Omron) and the deferred MELSEC are tracked in the program design doc §6/§8,
> not here. Omron CIP is the program's sole real-hardware wire gate; BACnet's broadcast/BBMD leg is
> env-gated live. Both are out of this file's scope until they're pulled forward.
---
## Executing a plan (git worktree + subagent-per-task)
Each 📝 plan is executed with the **subagent-driven-development** skill: it sets up an isolated
**git worktree** first (via `using-git-worktrees`), then dispatches a **fresh subagent per task**,
running the classification-driven review chain (`trivial` = implement only … `high-risk` =
spec-review → code-review → integration review) between tasks. The `.tasks.json` next to each plan
tracks progress and lets a later session resume.
> ⚠️ **All four plans below have been executed and merged.** This table is retained as the *pattern*
> for executing a future driver plan — do **not** run these four commands, they would rebuild shipped
> drivers in a fresh worktree. (Their `.tasks.json` files still read `pending`; that is stale
> bookkeeping, not backlog — see `deferment.md` §6.1.)
| Deliverable | Status | Command (pattern only — do not re-run) |
|---|---|---|
| Modbus RTU (Wave 1) | ✅ merged `0f38f486` | ~~`… execute docs/plans/2026-07-24-modbus-rtu-driver.md …`~~ |
| SQL poll (Wave 1) | ✅ merged `4ad54037` | ~~`… execute docs/plans/2026-07-24-sql-poll-driver.md …`~~ |
| MTConnect (Wave 2) | ✅ merged `90bdaa44` (#506) | ~~`… execute docs/plans/2026-07-24-mtconnect-driver.md …`~~ |
| MQTT/Sparkplug (Wave 2) | ✅ merged `c3a2b0f7` | ~~`… execute docs/plans/2026-07-24-mqtt-sparkplug-driver.md …`~~ |
For a **new** driver the invocation shape is:
`Use superpowers-extended-cc:subagent-driven-development to execute <plan-path> in a new git worktree`
- **Slash-command form** (equivalent): `/superpowers-extended-cc:subagent-driven-development <plan-path>`.
- **Parallel-session / resume form** (batch execution with checkpoints, no fresh-subagent-per-task):
`/superpowers-extended-cc:executing-plans <plan-path>` — reads the same `.tasks.json` and continues
from the first pending task.
- **Order (historical):** the four were built lowest-effort-first — Modbus RTU → SQL poll → MTConnect
→ MQTT/Sparkplug. All are merged; nothing in this list remains to schedule. Run one plan per
worktree; independent plans may run concurrently in separate worktrees.
---
## Wave 0 — Universal Discover-backed browser ✅
**What it is.** One generic `DiscoveryDriverBrowser` (+ `CapturingAddressSpaceBuilder`,
`CapturedTreeBrowseSession`, `BrowserSessionService` fallback, and the
`ITagDiscovery.SupportsOnlineDiscovery` gate) that turns any driver's `ITagDiscovery.DiscoverAsync`
into an AdminUI browse tree. It is the **Wave-0 gate**: every browsable new driver depends on this
seam, and it retrofits browse to the already-shipped AbCip / TwinCAT / FOCAS drivers for near-zero
marginal cost.
- **Design:** [`2026-07-15-universal-discovery-browser-design.md`](2026-07-15-universal-discovery-browser-design.md)
- **Implementation plan:** [`2026-07-15-universal-discovery-browser-implementation.md`](2026-07-15-universal-discovery-browser-implementation.md) · [`.tasks.json`](2026-07-15-universal-discovery-browser-implementation.md.tasks.json)
- **Status: ✅ Done — merged, live gate CLOSED.**
- Merged to master — `056887d6` (*Merge feat/universal-discovery-browser — Wave-0 universal Discover-backed browser*), 2026-07-15.
- Implementation plan: **19 / 19 tasks completed.**
- Lit up AbCip / TwinCAT / FOCAS pickers with zero per-driver browse code.
- **Live gate (Gitea #468) — CLOSED 2026-08-01** (`c2c3c522`). The fixture block was removed by
teaching the libplctag `ab_server` sim to serve the Symbol Object `@tags` walk (vendored patches in
`tests/Drivers/…AbCip.IntegrationTests/Docker/patches/`); all three criteria then ran green on
docker-dev — populated nine-tag tree render, Refresh re-capture, and leaf commit round-trip. See
"Second live-gate result" in the [implementation plan](2026-07-15-universal-discovery-browser-implementation.md).
- **The gate earned its keep — three real defects fixed, none reachable by unit tests:**
1. `CipSymbolObjectDecoder` padded to even byte boundaries between packed entries, so any
odd-length symbol name silently dropped every tag behind it (the test builder padded
identically, so all tests agreed with each other and none with a controller).
2. **Gitea #536** — browse-commit discarded the discovered data type (a browsed REAL committed as
`DInt`), fixed by carrying the driver's own type name via `NativeDataType`; affected
AbCip/TwinCAT/FOCAS.
3. AbCip Test Connect misclassified the no-authored-tags probe **on real hardware too** — the
reachable-exception matcher looked for native C status spellings libplctag.NET never emits
(plus a sim-side 0x03/0x04 CIP status mismatch).
- **Residual follow-up — CLOSED 2026-08-05.** The named `*DriverPage` shells were already deleted
when v3 Batch 2 retired the routed `/clusters/{id}/drivers` flow, but the defect pattern had
survived into the v3 hosts: `RawTagModal` and `RawManualTagEntryModal` passed a `"{}"` stub as
`GetDriverConfigJson`, so the AbCip/TwinCAT/FOCAS pickers' "Browse controller" captured against
no endpoint. Fixed by resolving `LoadMergedProbeConfigAsync` per open and forwarding a live
accessor (design + outcome: `2026-08-05-raw-modal-picker-live-config-design.md`). Review of the
fix surfaced and closed a further stale-open race (a superseded open could install another
device's config); `RawBrowseModal` shares that pre-existing race shape and is recorded in
`deferment.md` §0.1, not fixed here. Pinned by `RawTagModalMergedConfigTests` +
`RawManualTagEntryModalMergedConfigTests`, and **live-gated 2026-08-05** — all three legs PASSED
against the patched AbCip fixture (edit modal + manual entry both browse the nine-tag tree; an
unreachable device degrades to the error chip and still saves). The gate also found **Gitea #539**:
the AbCip *discovery* path drops the authored EtherNet/IP port, so a device on a non-default port
browses whatever answers on 44818 — #534's defect surviving in `LibplctagTagEnumerator` +
`LibplctagTemplateReader`.
---
## Wave 1 — low-effort / high-leverage pair ✅
Both are **fully CI-simulatable with zero new hardware**; Modbus RTU needs no new infra at all, and
SQL poll reuses the always-on central SQL Server. **Both shipped.**
### Modbus RTU — ✅ Done (merged `0f38f486` / #495, live gate PASSED)
- **Design:** [`2026-07-15-modbus-rtu-driver-design.md`](2026-07-15-modbus-rtu-driver-design.md)
- **Implementation plan:** [`2026-07-24-modbus-rtu-driver.md`](2026-07-24-modbus-rtu-driver.md) · [`.tasks.json`](2026-07-24-modbus-rtu-driver.md.tasks.json) — **11 tasks** (1 trivial / 5 small / 2 standard / 3 high-risk), all complete via subagent-driven-development.
- **Scope:** extend the existing Modbus driver with a `Transport` selector (RTU CRC-16 + FC-aware
length, no TxId) + a `rtu_over_tcp` `pymodbus` docker profile + the AdminUI selector.
**Direct-serial transport is descoped** (user, 2026-07-15) — RTU-over-TCP via a serial→Ethernet
gateway is the only shipped mode, so there is no serial hardware gate.
- **Fixture:** added the `rtu_over_tcp` profile (`framer=rtu`, host `:5021`, co-runs with `standard`)
to the Modbus integration fixture. **Unknown confirmed:** pymodbus 3.13's simulator DOES honour
`framer=rtu` on a TCP server (wire-probed a pure RTU FC03 response, no MBAP) — so the simple profile
path was taken; **no stdlib fallback server was needed.**
- **Verification (all green):** unit suite **344/344**; full solution build 0 errors; the 2 RTU
integration tests pass **live** against the real `framer=rtu` fixture (read HR5→5, write+readback
HR200←4242). Per-task review chain (spec + code reviewers) + a final whole-branch review, all
approved-to-merge.
- **Live `/run` gate — PASSED (2026-07-24, docker-dev rebuilt from this branch):**
- AdminUI `/raw` Modbus **driver** config modal renders the new **Transport** selector (with the
operator hint) — set to `RtuOverTcp`, saved.
- **Device Test Connect → `OK · 23 MS`** — the probe built a `ModbusRtuOverTcpTransport` via the
factory and spoke **FC03 over real RTU framing** to `10.100.0.35:5021`.
- Authored raw tags HR5 (r/o) + HR200 (r/w) → **deployment `06ec1632` Sealed** (all 6 nodes acked).
- Client.CLI on `opc.tcp://localhost:4840` (`multi-role`/`password`): **read** `ns=2;s=rtu-gate/Device1/HR5`
→ **5, Good**; **write** `ns=2;s=rtu-gate/Device1/HR200` = 4242 → readback **4242, Good**.
- **Effort:** **S — the lowest on the roadmap.** Delivered first, as recommended.
### SQL poll — ✅ Done (merged `4ad54037`, live gate PASSED)
- **Design:** [`2026-07-15-sql-poll-driver-design.md`](2026-07-15-sql-poll-driver-design.md)
- **Implementation plan:** [`2026-07-24-sql-poll-driver.md`](2026-07-24-sql-poll-driver.md) · [`.tasks.json`](2026-07-24-sql-poll-driver.md.tasks.json) — **22 tasks, all done**. `ISqlDialect` + `SqlServerDialect` shipped. ~~Postgres/ODBC remain deferred to P2/P3 behind the same seam.~~ **Postgres + Oracle shipped 2026-08-05** via managed providers (Npgsql 10.0.3, ODP.NET Core 23.26.300) — dialect-owned parameter markers/bind names + Oracle `BindByName`, `INFORMATION_SCHEMA` vs `ALL_*` catalog SQL, provider gates widened, AdminUI dropdown, env-gated docker suites (postgres:17-alpine / gvenzl/oracle-free:23-slim). The live suites found + fixed ORA-01745 (Oracle rejects reserved-word bind names — `:table` — so the first Oracle build was structurally broken until binds were `p_`-prefixed). MySql/Odbc stay reserved. See [`2026-08-05-sql-postgres-oracle-design.md`](2026-08-05-sql-postgres-oracle-design.md).
- **Post-merge follow-ups closed** in `28c28667`: #496 catalog identifier-validation gate
(`4dc2ad80`), #497 wrong status-code constants across six drivers, #498 deploy-gate rejection of a
persisted `connectionString` (`1919a8e5`).
- **Scope:** a bespoke schema-browser driver polling a SQL table into equipment tags (SQL Server,
**PostgreSQL and Oracle** shipped behind `ISqlDialect`; MySql/Odbc reserved).
- **Fixture:** SQLite unit fixture (primary) + an **env-gated integration fixture against the
existing central SQL Server** (`10.100.0.35,14330`) with a seeded `SqlPollFixture` DB. The
blackhole/timeout live-gate pauses a **dedicated** `mssql` container — **never** the shared
central SQL Server (it hosts `ConfigDb`).
- **Effort:** S–M.
---
## Wave 2 — strategic telemetry / UNS pair ✅
Both CI-simulatable on the shared docker host, no hardware.
### MTConnect Agent — ✅ Done (P1 Agent MVP)
- **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; 22 built, Task 21 (live `/run` gate) tracked separately in the plan file.**
- **Driver guide:** [`docs/drivers/MTConnect.md`](../drivers/MTConnect.md) — config keys, capability
surface, data-plane precedence rules, quality-code mapping, and (most load-bearing) the **Known
limitations** section.
- **Scope shipped:** `IDriver`+`ITagDiscovery`+`IReadable`+`ISubscribable`+`IHostConnectivityProbe`+`IRediscoverable`
(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).
- **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.
- **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
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)
- **Design:** [`2026-07-15-mqtt-sparkplug-driver-design.md`](2026-07-15-mqtt-sparkplug-driver-design.md)
- **Implementation plan:** [`2026-07-24-mqtt-sparkplug-driver.md`](2026-07-24-mqtt-sparkplug-driver.md) · [`.tasks.json`](2026-07-24-mqtt-sparkplug-driver.md.tasks.json) — **27 tasks, all done** (P1 plain MQTT = Tasks 0–14; P2 Sparkplug B = Tasks 15–26).
- **Scope delivered:** P1 plain MQTT (MQTTnet-5 connect/TLS/auth + hand-rolled reconnect, subscribe→
`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).
**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
(`MQTT_FIXTURE_ENDPOINT`, see `infra/README.md` §3). The **project-owned C# Sparkplug edge-node
simulator** (`--profile sparkplug`, group `OtOpcUaSim`, nodes `EdgeA`/`EdgeB`, `EdgeA` device
`Filler1`) shipped with P2 and answers rebirth NCMDs.
- **P2 status (2026-07-25):** Tasks 15–26 complete. **Offline 1528 passed / 0 failed**
(581 driver · 809 AdminUI · 138 Core.Abstractions); **live 15/15** against the broker + simulator.
- **P1 live gate found two AdminUI authoring gaps** (the gate's whole point — both invisible to green
unit tests, because AdminUI has no bUnit and nothing tied these hand-maintained lists to
`DriverTypeNames`):
1. **FIXED —** `RawDriverTypeDialog`'s option array never got an MQTT row, so **no operator could
create an MQTT driver at all**. Fixed, plus a reflection parity guard
(`RawDriverTypeDialogParityTests`) that fails for *any* future `DriverTypeNames` entry
missing from the picker.
2. **FIXED —** `MqttDriverForm` / `MqttDeviceForm` did not exist, so the broker connection was
unauthorable from the AdminUI. Both shipped after the P1 gate.
- **P2 live gate found a third one, of the same family — FIXED:** `MqttDriverForm` still carried its
P1 Sparkplug **placeholder**. Switching Mode to `SparkplugB` rendered a *"not available yet"* notice
and **no Group ID field** — so once Sparkplug ingest shipped there was still **no way to author a
Sparkplug driver from the AdminUI**, and `Sparkplug.GroupId` is the driver's entire subscription
filter (`spBv1.0/{GroupId}/#`): blank ⇒ connected, `Healthy`, ingesting nothing. Fixed (all five
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 — 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
any browse-tree node label navigated the whole AdminUI to `/`**, tore down the circuit and destroyed
the hosting modal — losing the browse session and the tag selection. `@onclick:preventDefault` does
not help: it suppresses the browser's default action, not Blazor's own interceptor. The labels are
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).
- **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
reporting `Healthy`. Unset (the default) is correct. `MqttDriverForm` warns inline the moment a
value is typed.
- **Effort:** M–L, delivered. The MQTTnet-5/net10 + central-pinning risk is **retired** (Task 0's
spike held through both phases); Sparkplug state-machine correctness was carried by golden-payload
vectors + the live simulator.
---
## Next actions
1. **Wave 0 is done.** The live gate (Gitea #468) CLOSED 2026-08-01 (`c2c3c522`) — browse
verification is no longer a blocker for BACnet. The residual follow-up (the picker config
pass-through) CLOSED 2026-08-05 — see the Wave 0 section above.
2. **Wave 1 is done.** Both deliverables shipped and live-gated: **Modbus RTU-over-TCP** (merged
`0f38f486` / #495) and **SQL poll** (merged `4ad54037`; follow-ups #496/#497/#498 closed in
`28c28667`). **Postgres + Oracle dialects shipped 2026-08-05** (managed providers, see the SQL
poll section above); remaining SQL work is optional follow-on — MySql/Odbc dialects behind
`ISqlDialect`, and `SqlTagModel.Query` (P3, currently rejected end-to-end by design).
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.