28 KiB
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.mdLast 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 | plan · tasks | ✅ 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 | plan · tasks | ✅ 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 | plan · tasks | ✅ 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 | plan · tasks | ✅ 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 | plan · tasks | ✅ 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.jsonfiles still readpending; that is stale bookkeeping, not backlog — seedeferment.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.jsonand 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 - Implementation plan:
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.
- Merged to master —
- Live gate (Gitea #468) — CLOSED 2026-08-01 (
c2c3c522). The fixture block was removed by teaching the libplctagab_serversim to serve the Symbol Object@tagswalk (vendored patches intests/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. - The gate earned its keep — three real defects fixed, none reachable by unit tests:
CipSymbolObjectDecoderpadded 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).- Gitea #536 — browse-commit discarded the discovered data type (a browsed REAL committed as
DInt), fixed by carrying the driver's own type name viaNativeDataType; affected AbCip/TwinCAT/FOCAS. - 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
*DriverPageshells were already deleted when v3 Batch 2 retired the routed/clusters/{id}/driversflow, but the defect pattern had survived into the v3 hosts:RawTagModalandRawManualTagEntryModalpassed a"{}"stub asGetDriverConfigJson, so the AbCip/TwinCAT/FOCAS pickers' "Browse controller" captured against no endpoint. Fixed by resolvingLoadMergedProbeConfigAsyncper 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);RawBrowseModalshares that pre-existing race shape and is recorded indeferment.md§0.1, not fixed here. Pinned byRawTagModalMergedConfigTests+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 inLibplctagTagEnumerator+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 - Implementation plan:
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
Transportselector (RTU CRC-16 + FC-aware length, no TxId) + artu_over_tcppymodbusdocker 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_tcpprofile (framer=rtu, host:5021, co-runs withstandard) to the Modbus integration fixture. Unknown confirmed: pymodbus 3.13's simulator DOES honourframer=rtuon 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=rtufixture (read HR5→5, write+readback HR200←4242). Per-task review chain (spec + code reviewers) + a final whole-branch review, all approved-to-merge. - Live
/rungate — PASSED (2026-07-24, docker-dev rebuilt from this branch):- AdminUI
/rawModbus driver config modal renders the new Transport selector (with the operator hint) — set toRtuOverTcp, saved. - Device Test Connect →
OK · 23 MS— the probe built aModbusRtuOverTcpTransportvia the factory and spoke FC03 over real RTU framing to10.100.0.35:5021. - Authored raw tags HR5 (r/o) + HR200 (r/w) → deployment
06ec1632Sealed (all 6 nodes acked). - Client.CLI on
opc.tcp://localhost:4840(multi-role/password): readns=2;s=rtu-gate/Device1/HR5→ 5, Good; writens=2;s=rtu-gate/Device1/HR200= 4242 → readback 4242, Good.
- AdminUI
- 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 - Implementation plan:
2026-07-24-sql-poll-driver.md·.tasks.json— 22 tasks, all done.ISqlDialect+SqlServerDialectshipped.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 + OracleBindByName,INFORMATION_SCHEMAvsALL_*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 werep_-prefixed). MySql/Odbc stay reserved. See2026-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 persistedconnectionString(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 seededSqlPollFixtureDB. The blackhole/timeout live-gate pauses a dedicatedmssqlcontainer — never the shared central SQL Server (it hostsConfigDb). - 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 - Implementation plan:
2026-07-24-mtconnect-driver.md·.tasks.json— 23 tasks; 22 built, Task 21 (live/rungate) tracked separately in the plan file. - Driver guide:
docs/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 withoutIWritable;IWritableadded 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→BadNoCommunicationmapping, CONDITION-as-String, ring-buffer re-baseline paging (both the sequence-gap andOUT_OF_RANGElegs). - Diverged from the design: Task 0/6/7 dropped the planned TrakHound
MTConnect.NET-Common/-HTTPdependency 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-rolledSystem.Xml.Linq, matching onLocalNameonly (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, notmtconnect/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, branchfeature/mqtt-mtconnect-write; design2026-08-05-mtconnect-writeback-design.md).IWritableover the Agent's optionalAllowPutsurface (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 UACurrentWritebit still comes from the authoredTag.AccessLevelalone, so an operator must author ReadWrite. - Deferred (documented, not built): MTConnect Interfaces (design §3.6, above); P1.5
CONDITION→native Part-9 alarms,
TIME_SERIESarray 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/IHostConnectivityProbehave 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 ownDocker/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 - Implementation plan:
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.Toolscodegen, 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, live-gated): plainwriteTopicpublish + Sparkplug NCMD/DCMD, with a node discoveredViewOnlywhen the encoder would refuse the write. Superseded the "every MQTT node materializes read-only" line this entry carried; seedocs/drivers/Mqtt.md§Write-through. - Fixture: Mosquitto TLS+auth broker + JSON publisher sidecar, live at
10.100.0.35:8883(:1883plaintext-but-authenticated); stack/opt/otopcua-mqtt, compose intests/Drivers/….Driver.Mqtt.IntegrationTests/Docker/. Env-gated live suite (MQTT_FIXTURE_ENDPOINT, seeinfra/README.md§3). The project-owned C# Sparkplug edge-node simulator (--profile sparkplug, groupOtOpcUaSim, nodesEdgeA/EdgeB,EdgeAdeviceFiller1) 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):- 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 futureDriverTypeNamesentry missing from the picker. - FIXED —
MqttDriverForm/MqttDeviceFormdid not exist, so the broker connection was unauthorable from the AdminUI. Both shipped after the P1 gate.
- FIXED —
- P2 live gate found a third one, of the same family — FIXED:
MqttDriverFormstill carried its P1 Sparkplug placeholder. Switching Mode toSparkplugBrendered 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, andSparkplug.GroupIdis 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 falsifiableMqttDriverFormModelTestscases. 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; seedocs/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). 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.ResolveRebirthTargetsalready 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 —RawBrowseModalnow offers agroup/edgeNodescope input whenever no tree scope is armed; edge-node only, never a typed group broadcast. - A metric name containing
/cannot be browse-committed (#509). The derived raw tag Name is a RawPath segment and may not contain/, whilemetricNamelegitimately may — so the spec-mandatoryNode Control/Rebirthwas 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.ToRawSegmentsanitises the segment and refuses a collision explicitly. _canRebirthis captured at browse-open, so a reaped session still drew the button (#510). FIXED.- The tag editor injects the Plain-only
payloadFormatkey into a Sparkplug blob (#513) — cosmetic; the factory routes on the Sparkplug tuple and ignored it. FIXED.
- A completely empty browse tree cannot arm a rebirth
(#514). The scope comes from a tree
click, so on a plant that has not birthed since the window opened
- P2 live gate also found a PRE-EXISTING, cross-driver AdminUI defect — FIXED: every node label in
the shared
DriverBrowseTreewas 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:preventDefaultdoes 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
OnRediscoveryNeededon a changed birth metric-set; at the time nothing subscribed to it andDriverHostActor.HandleDiscoveredNodeshard-returned.DriverInstanceActornow consumes the raise (09a401b8) and it rides the driver-health snapshot to the AdminUI/hostspage 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
clientIdmakes them evict each other forever — the broker logsalready connected, closing old connectionand both nodes reconnect every ~2 s while still reportingHealthy. Unset (the default) is correct.MqttDriverFormwarns 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
-
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. -
Wave 1 is done. Both deliverables shipped and live-gated: Modbus RTU-over-TCP (merged
0f38f486/ #495) and SQL poll (merged4ad54037; follow-ups #496/#497/#498 closed in28c28667). 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 behindISqlDialect, andSqlTagModel.Query(P3, currently rejected end-to-end by design). -
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): plainwriteTopicpublish- Sparkplug NCMD/DCMD.
- Both browse-UI gaps are closed, with the other two P2 records, in
7f53bd96(#509, #510, #513, #514) — see the P2 section above. - MTConnect write-back is landing on
feature/mqtt-mtconnect-write(b29623cf), by observation injection through the Agent'sAllowPutsurface rather than the Interfaces handshake the original deferral assumed — design2026-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_SERIESarrays, EVENT→enum vocab) and P2 SHDR ingest. - MQTT P3 write-through shipped and was live-gated 2026-07-31 —
Update this file's Summary table and per-wave status whenever a deliverable changes state.