339 lines
28 KiB
Markdown
339 lines
28 KiB
Markdown
# 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.
|