docs: driver-expansion program — 8 research reports + 7 design docs, parallel-reviewed
v2-ci / build (push) Successful in 3m14s
v2-ci / unit-tests (push) Failing after 9m13s

Adds the driver-expansion program design (umbrella: universal Discover-backed
browser + MTConnect, MQTT/Sparkplug B, BACnet/IP, SQL poll, Omron, Modbus RTU;
MELSEC deferred) plus the per-driver research reports.

All docs went through a 7-agent parallel review against the codebase before
this commit. Highlights fixed in review:

- universal browser: FOCAS FixedTree fills post-connect -> UntilStable settle
  + FixedTree.Enabled patch; MQTT reconciled to bespoke (was contradicting the
  program doc's SupportsOnlineDiscovery=false verdict)
- modbus-rtu: SerialPort.ReadTimeout doesn't bound async BaseStream reads ->
  linked-CTS per-op deadline (R2-01 class); BCL enum reuse would leak
  System.IO.Ports into Contracts
- bacnet: DiscoveryRediscoverPolicy enum name; UDP 47808 contention; live
  suite rewritten around unicast Who-Is + BBMD (broadcast doesn't cross VMs)
- sql-poll: real tier registration via DriverFactoryRegistry.Register;
  blackhole gate must not docker-pause the shared central SQL Server
- mqtt: Sparkplug v3.0 STATE topic form; first-in-repo proto codegen noted
- omron: host hardcodes isIdempotent:false today (retry seam unshipped);
  v1 scopes UDTs to dotted-leaf access
- mtconnect: SecurityClassification.ViewOnly; factory ParseEnum<T> pattern
- program doc: both valid enum-serialization patterns; IRediscoverable is
  change-signal-gated; RTU P2 adds System.IO.Ports; label is host-side
This commit is contained in:
Joseph Doherty
2026-07-15 16:40:36 -04:00
parent 37cdbef7a5
commit 8fc147d8d4
17 changed files with 6408 additions and 0 deletions
+151
View File
@@ -0,0 +1,151 @@
# Next-driver research — index & consolidated roadmap
> **Overall program design:** [`docs/plans/2026-07-15-driver-expansion-program-design.md`](../../plans/2026-07-15-driver-expansion-program-design.md)
> is the authoritative umbrella — shared architecture, cross-cutting rules, the browse-tier
> decision, the dependency graph, and the wave build order. Start there; this file is the
> research/design index it points back to.
Research sweep run 2026-07-15 (8 parallel agents). Each per-driver report covers
protocol + .NET library options (with license analysis), capability mapping to the
Equipment-kind driver seams, `TagConfig` JSON shape, a **browseability verdict +
`IDriverBrowser`/`IBrowseSession` design where browsable**, test-fixture strategy,
and effort/phasing. This index synthesizes them.
Scope decided with the user (2026-07-15): 7 new drivers + a browse-support audit of
the existing driver fleet. See `[[project_next_driver_roadmap]]` memory.
## Reports
| Report | Driver |
|---|---|
| [`mtconnect-agent.md`](mtconnect-agent.md) | MTConnect (Agent-first) |
| [`mqtt-sparkplug.md`](mqtt-sparkplug.md) | MQTT / Sparkplug B |
| [`melsec-slmp.md`](melsec-slmp.md) | Mitsubishi MELSEC SLMP / MC Protocol |
| [`omron.md`](omron.md) | Omron (CIP + FINS) |
| [`bacnet-ip.md`](bacnet-ip.md) | BACnet/IP |
| [`sql-poll.md`](sql-poll.md) | SQL poll |
| [`modbus-rtu.md`](modbus-rtu.md) | Modbus RTU (extend existing Modbus) |
| [`00-existing-driver-browse-audit.md`](00-existing-driver-browse-audit.md) | Browse audit of the 6 shipped browser-less drivers |
## Design docs (executable — built from the reports 2026-07-15)
Each non-deferred driver now has a build-ready design under `docs/plans/`:
| Driver | Design doc | Browse integration |
|---|---|---|
| Universal browser (Wave 0) | [`2026-07-15-universal-discovery-browser-design.md`](../../plans/2026-07-15-universal-discovery-browser-design.md) | — (the mechanism itself) |
| MTConnect Agent | [`2026-07-15-mtconnect-driver-design.md`](../../plans/2026-07-15-mtconnect-driver-design.md) | **Universal** (`SupportsOnlineDiscovery`) |
| BACnet/IP | [`2026-07-15-bacnet-ip-driver-design.md`](../../plans/2026-07-15-bacnet-ip-driver-design.md) | **Universal** (small sites); bespoke-lazy P-later |
| MQTT/Sparkplug B | [`2026-07-15-mqtt-sparkplug-driver-design.md`](../../plans/2026-07-15-mqtt-sparkplug-driver-design.md) | **Bespoke** observation browser |
| SQL poll | [`2026-07-15-sql-poll-driver-design.md`](../../plans/2026-07-15-sql-poll-driver-design.md) | **Bespoke** schema browser |
| Omron | [`2026-07-15-omron-driver-design.md`](../../plans/2026-07-15-omron-driver-design.md) | None — offline Sysmac tag-export import |
| Modbus RTU | [`2026-07-15-modbus-rtu-driver-design.md`](../../plans/2026-07-15-modbus-rtu-driver-design.md) | None (flat registers) — extends existing Modbus |
Browse split confirmed by the designs: **universal** covers MTConnect + BACnet (from `DiscoverAsync`);
**bespoke** required for MQTT (passive observation window) and SQL (schema walk ≠ discovery);
**Omron/Modbus-RTU** don't browse. All designs consume the universal browser's P1 addition of
`ITagDiscovery.SupportsOnlineDiscovery` — build the universal browser first.
## New drivers — at a glance
| Driver | Library (license) | Read/Write | Browsable | Write in v1 | Effort |
|---|---|---|---|---|---|
| **MTConnect Agent** | TrakHound MTConnect.NET (MIT) | Read-only | **Yes**`/probe` device model | No (telemetry std) | SM (~11.5 wk) |
| **MQTT / Sparkplug B** | MQTTnet v5 (MIT) + SparkplugNet (MIT, net10 risk) or hand-rolled Tahu | Read + subscribe | **Yes** — birth certs / topic tree (passive, time-bounded) | No | M |
| **MELSEC SLMP** _(deferred)_ | Hand-roll 3E-binary framer (HslCommunication is a commercial-license blocker) | Read/Write | No — flat device memory | Yes | M (addressing) |
| **Omron** | libplctag.NET (MPL-2.0) for CIP + hand-rolled FINS | Read/Write | CIP conditional (net-new) / FINS no | Yes | ML (no free sim) |
| **BACnet/IP** | System.IO.BACnet / ela-compil (MIT, net10 TFM) | Read + COV subscribe | **Yes** — Who-Is + object-list | No (write P3) | ML |
| **SQL poll** | Microsoft.Data.SqlClient (already in-repo) | Read-only | **Yes** — INFORMATION_SCHEMA | No (write later) | SM |
| **Modbus RTU** | Extend existing Modbus (new `IModbusTransport`) | Read/Write | No — flat registers | Yes | **S (lowest)** |
Notes:
- **All fit the existing all-.NET-10 Equipment-kind model** — no COM gateway.
- **MELSEC**: `HslCommunication` is a hard commercial-license blocker; the clean-license
alternatives (McpX/MIT, McProtocol/LGPL) are too immature → hand-roll, patterned on S7.
Repo already has `MelsecAddress`/`MelsecFamily` (from the Modbus-path work) to port.
- **Omron CIP** reuses ~7080% of AbCip's libplctag body, **but** libplctag's `@tags`
walker returns `ErrorUnsupported` on Omron NJ/NX — CIP browse is net-new Omron work
gated on the Sysmac "Network Publish" attribute, not free reuse.
- **MQTT/Sparkplug** & **BACnet** discovery is **passive/time-bounded** (listen/broadcast
over a window), unlike a synchronous OPC UA browse — the browse session handles this
with an observation window (+ Sparkplug rebirth NCMD to force near-synchronous enum).
- Every new driver needs a typed tag editor (`TagConfigEditorMap`+`TagConfigValidator`)
and must apply `JsonStringEnumConverter` on enum config fields (the known
`[[project_driver_enum_serialization_bug]]` trap).
## Existing-driver browse audit — verdicts
| Driver | Browsable? | Mechanism | Action |
|---|---|---|---|
| **AbCip** (ControlLogix EtherNet/IP) | **Yes** | CIP Symbol Object 0x6B + Template 0x6C — decode stack already ships | **Add browser** (~23 d) |
| **TwinCAT** (Beckhoff ADS) | **Yes** | ADS symbol upload (`SymbolLoaderFactory`) — enumerator already ships | **Add browser** (~34 d) |
| **FOCAS** (Fanuc CNC) | Borderline | Curated, not free-form (axes/spindles + macro/param/PMC ranges) | Curated browser, **deferred** (~35 d) |
| **AbLegacy** (PCCC) | No | Numeric addresses (`N7:0`), no symbolic catalog | Skip (documented) |
| **S7** (S7comm) | No | Symbols live in the TIA project, not the CPU | Skip (use OpcUaClient for optimized DBs) |
| **Modbus** (TCP/RTU) | No | Flat register space, no discovery | Skip (documented) |
**Key finding:** AbCip & TwinCAT already contain complete, unit-tested symbol-enumeration
logic buried in the runtime driver's `DiscoverAsync` (gated by `EnableControllerBrowse` /
Flat-mode) and simply not surfaced as the interactive `IDriverBrowser` the AdminUI picker
consumes. Their browsers are a **wrapping job over existing code**, not new protocol work —
the razor picker bodies already exist as static manual-entry stubs.
**Shareable code:** the CIP browse core (0x6B/0x6C decoders + system-tag filter + UDT
layout) is protocol-CIP, not Rockwell-specific — extract a shared `Cip.Core` lib **only
if/when** Omron CIP browse lands (don't pre-abstract). Both AbCip & TwinCAT already expose
their enumerator behind an interface = the ready-made reuse seam.
## Unified browse-support picture
**Browsers to build (net + existing), by protocol richness:**
- Synchronous, model-rich: **AbCip**, **TwinCAT**, **MTConnect** (`/probe`), **SQL** (schema), **BACnet** (object-list), **Omron-CIP**.
- Passive/time-bounded: **MQTT/Sparkplug** (observation window / rebirth).
- Curated/fixed surface: **FOCAS** (deferred).
**Not browsable — documented, decision recorded so it isn't revisited:**
MELSEC SLMP · Modbus TCP · Modbus RTU · S7 · AbLegacy · Omron-FINS (all flat address spaces / no on-wire symbol table).
## Recommended build order (value × effort)
**Wave 0 — universal Discover-backed browser (the foundational browse piece):**
1. **Universal `DiscoveryDriverBrowser`** — ONE generic `IDriverBrowser` that runs any
discovery-capable driver's `ITagDiscovery.DiscoverAsync` against a capturing
`IAddressSpaceBuilder` and serves the captured tree back through the picker. Lights up
AbCip, TwinCAT, FOCAS, MTConnect, BACnet (small sites), authored-MQTT, and every future
discovery driver's picker with **zero per-driver browser code**. Gated on a new
online-discovery capability flag so flat-address drivers stay manual-entry. Design:
[`docs/plans/2026-07-15-universal-discovery-browser-design.md`](../../plans/2026-07-15-universal-discovery-browser-design.md).
2. **AbCip / TwinCAT** — covered *immediately* by the universal browser (their enumeration
already lives in `DiscoverAsync`). Graduate each to a **bespoke lazy `IBrowseSession`**
only if a real controller's symbol set is too large for the universal browser's eager
one-shot discovery (UDT drill-down / per-click expand). Not up-front work anymore.
**Wave 1 — low-effort, high-leverage new drivers:**
3. **Modbus RTU** — lowest-effort new capability; extends the existing driver via one new `IModbusTransport` (lead with RTU-over-TCP for the containerized server).
4. **SQL poll** — zero new NuGet deps (SqlClient already in-repo), broad MES/ERP glue value, browsable.
**Wave 2 — strategic new drivers:**
5. **MTConnect Agent** — MIT library, clean browse, complements FOCAS with vendor-neutral machine-tool telemetry.
6. **MQTT / Sparkplug B** — UNS-strategic; plain-MQTT first to stand up the skeleton + validate MQTTnet/net10, then Sparkplug birth-driven discovery.
**Wave 3 — heavier / test-gated:**
7. **BACnet/IP** — opens facilities/HVAC/energy; COV + discovery + BBMD are the meaty parts (live-gate the broadcast reachability).
8. **Omron** — CIP-first (libplctag) then FINS; last of the active set because there's **no free NJ/NX CIP simulator**, so real-wire correctness is a live-hardware `LiveIntegration` gate.
**Deferred (research complete, not scheduled):**
- **MELSEC SLMP** — deferred (2026-07-15). The mature library is a commercial-license
blocker so it's a full hand-rolled 3E-binary framer with the hardest addressing model
on the roadmap (device-code × bit/word × hex/octal/decimal × word order); revisit when
a Mitsubishi-shop customer need makes it worth the build. Report stays valid as the
starting point.
FOCAS browser slots opportunistically whenever the CNC surface is being touched.
## Per-driver add pattern (reference)
`*.Contracts` project + driver impl (Connect/Discover/Read/Subscribe[/Write]) + factory +
`DriverType` string + typed tag editor (`TagConfigEditorMap`+`TagConfigValidator`) +
optional `*.Browser` project (register one line at
`EndpointRouteBuilderExtensions` AddAdminUI DI) + docker fixture (`project=lmxopcua`
label). Watch the enum-serialization trap and add a per-query/-op timeout deadline
(the R2-01 frozen-peer lesson) on every network call.