docs(driver-expansion): resolve all open design concerns from the review pass
v2-ci / build (push) Successful in 3m30s
v2-ci / unit-tests (push) Failing after 8m53s

Every concern the 7-agent review parked is now decided and integrated:

- universal browser: in-flight capture coalescing keyed (driverType,
  config-hash) + global cap of 4 concurrent captures; CanBrowse catches a
  throwing TryCreate and defensively shuts down the throwaway instance;
  cleanup ShutdownAsync bounded at 10s (R2-01); picker Refresh = close +
  re-capture. Coalesced sessions share an immutable tree, so DisposeAsync
  drops the session's reference, not the tree.
- mtconnect: UNAVAILABLE pinned to BadNoCommunication (fleet's
  BadCommunicationError stays reserved for the driver's own transport
  failures); TIME_SERIES sampleCount flows into ArrayDim; Subscribe timeout
  bounds only the stream-start handshake; library version/TFM folded into
  the license checklist.
- mqtt: browse rebirth is an explicit DriverOperator-gated "Request rebirth"
  button (RequestRebirthAsync(scope)) - never fired by open/root/expand;
  hand-rolled reconnect loop committed for P1 (MQTTnet v5 dropped
  ManagedMqttClient); Wave-2-start library re-verification checkbox;
  implement from the Sparkplug v3.0 spec text.
- bacnet: P1 opens with a first-spike checklist (package TFM, BBMD/
  segmented-RPM/COV API smoke, unicast-I-Am fixture behavior); AdminUI-node
  browse registers a second foreign device - BBMD FD-table sizing note.
- sql-poll: split-node browse needs env parity for Sql__ConnectionStrings__
  refs on admin nodes (actionable error + session-only pasted literal);
  one-row-per-key query contract (last-wins + rate-limited warning);
  absent key -> BadNoData; operationTimeout > commandTimeout authoring rule;
  Browser->Driver.Sql SqlClient transitive on AdminUI accepted on record.
- omron: FINS framer gated on W227 + live golden vectors; CIP string layout
  is the first P1 live-gate item; AbCip harness static-init anti-pattern
  note; writable-defaults-true operator warning.
- modbus-rtu: first P1 step confirms pymodbus exposes framer=rtu on a TCP
  server (custom-script fallback otherwise).
- program doc: new cross-cutting rule - driver ctors must be connection-free
  (the universal browser's CanBrowse throwaway instances depend on it).
This commit is contained in:
Joseph Doherty
2026-07-15 16:54:09 -04:00
parent cf03ca279d
commit fd0bec4ffe
8 changed files with 210 additions and 52 deletions
+49 -12
View File
@@ -74,7 +74,7 @@ src/Drivers/
| Path | Library | License | Notes |
|---|---|---|---|
| **CIP** | **`libplctag.NET`** (`<PackageReference Include="libplctag"/>`), attribute string `plc=omron-njnx` | native core dual MPL-2.0 / LGPL-2.1; .NET wrapper **MPL-2.0** | **Zero new license review** — the identical dependency AbCip already ships. |
| **FINS** | **Hand-rolled** (no third-party dep) | our own code | FINS framing is ~200 lines, fully documented (W227 + Wireshark dissector); mirrors `ModbusTcpTransport`. |
| **FINS** | **Hand-rolled** (no third-party dep) | our own code | FINS framing is ~200 lines, documented (W227 + Wireshark dissector) but research-sourced — trusted only after the P2 validation gate (§10); mirrors `ModbusTcpTransport`. |
**Avoid `HslCommunication`.** The maintained builds are **commercial-paid**; the "MIT" claim in
casual sources refers to a stale, abandoned community fork
@@ -187,6 +187,12 @@ programmer set its **Network Publish** attribute (`Publish Only`) in **Sysmac St
it are invisible to any external client — including our reads. Document this loudly in
`docs/drivers/Omron.md`: a CIP read of an unpublished variable fails at the wire, not a config bug.
**Operator documentation task (`docs/drivers/Omron.md`, P1):** alongside the Network-Publish note
above, the driver doc must carry an explicit operator-facing warning that **`writable` defaults to
`true` when omitted** from a tag's config (the convention inherited from AbCip's parser, §5.4). On
a write-capable PLC driver, forgetting the field does not fail safe — it silently authors a
writable node. Operators must set `"writable": false` explicitly on every read-only point.
---
## 4. Browse story — reconciliation with the universal browser
@@ -337,6 +343,11 @@ via `EquipmentTagRefResolver`. For an **equipment tag**, `FullName` **is the raw
`AbCipDataTypeExtensions`) maps to `DriverDataType`; `SecurityClass` = `Operate` when `writable` else
`ViewOnly`.
**`writable` defaults to `true` when absent** — the parser inherits AbCip's convention, on both
transport shapes (§5.2/§5.3). An Omron tag authored without an explicit `writable` field is
therefore a **writable** node (`SecurityClass = Operate`). On a write-capable PLC driver this
deserves an explicit operator-facing warning — see the operator-documentation item in §3.1.
---
## 6. Typed editor + validator
@@ -464,14 +475,26 @@ The honest gap: **there is no free NJ/NX CIP simulator.**
- **Primary CI surface — hand-rolled `IOmronCipRuntime` fake** (the same seam pattern AbCip uses via
`IAbCipTagRuntime`/factory injection). Drives read/write/decode/array/BOOL-in-word/status-mapping
unit tests with **no hardware and no container**. This is where correctness of the driver *body* is
proven.
proven. When mirroring AbCip's test structure, do **not** copy its integration-harness
profile-class pattern that had the textual static-initializer-order bug (`AbServerProfile.cs`,
fixed in `326b22a7`) — initialize profile constants free of cross-field static init-order
dependence.
- **Live gate — env-gated `Category=LiveIntegration`** against real NJ/NX hardware, skipping cleanly
when the env var is absent (the pattern the HistorianGateway live suite uses). This is the **only**
way to validate the real `plc=omron-njnx` libplctag wire path, Network-Publish behavior, and Omron
string/array/UDT quirks. Env vars e.g. `OMRON_CIP_ENDPOINT` / `OMRON_CIP_TEST_TAG` /
`OMRON_CIP_WRITE_SANDBOX_TAG`. **Flag as an infra-gated known limitation** in the driver doc + this
design until an NJ/NX is on the bench. (Omron's own CX-Simulator is Windows-only + license-gated in
CX-One — not a CI fixture.)
way to validate the real `plc=omron-njnx` libplctag wire path. **P1 live-hardware gate checklist,
in order:**
1. **CIP string layout — FIRST.** Omron NJ/NX's string layout differs from Logix, and whether
libplctag's `GetString` handles it correctly can only be proven on hardware. This is the
likeliest wire surprise, and the `IOmronCipRuntime` fake cannot cover it — run a `String`
read/write round-trip before anything else.
2. Basic read/write round-trips per `OmronDataType` over the real `plc=omron-njnx` path.
3. Network-Publish behavior (unpublished variable → wire failure, per §3.1).
4. Array reads/writes + dotted UDT-member access (§3.1's live-gate item).
Env vars e.g. `OMRON_CIP_ENDPOINT` / `OMRON_CIP_TEST_TAG` /
`OMRON_CIP_WRITE_SANDBOX_TAG` / `OMRON_CIP_STRING_SANDBOX_TAG`. **Flag as an infra-gated known
limitation** in the driver doc + this design until an NJ/NX is on the bench. (Omron's own
CX-Simulator is Windows-only + license-gated in CX-One — not a CI fixture.)
### FINS
- **Primary — hand-rolled byte-level `IFinsTransport` fake** (mirror the Modbus `IModbusTransport`
@@ -497,20 +520,34 @@ can't be proven in CI, so it should land when live NJ/NX hardware is available f
| Phase | Scope | Effort | Gate |
|---|---|---|---|
| **P1 — Omron CIP r/w (NJ/NX)** | `Driver.Omron` + `.Contracts`, `DriverType="Omron"`, `OmronCipTransport` on libplctag `plc=omron-njnx`; Connect/Read/Write/Subscribe(poll)/Probe; authored + equipment tags; typed editor + validator; factory + registration | LowMedium (7080% AbCip reuse) | hand-rolled runtime fake (CI) + env-gated live gate (hardware) |
| **P2 — Omron FINS r/w (CJ/CS/CP)** | `IFinsTransport` codec (TCP+UDP, `0x0101`/`0x0102`, area-code table, word-swap) behind the same `IDriver` shell; memory-area TagConfig + editor fields (no browse) | Medium (net-new codec, no dep/license risk) | byte-level fake (CI) + OSS FINS sim container |
| **P1 — Omron CIP r/w (NJ/NX)** | `Driver.Omron` + `.Contracts`, `DriverType="Omron"`, `OmronCipTransport` on libplctag `plc=omron-njnx`; Connect/Read/Write/Subscribe(poll)/Probe; authored + equipment tags; typed editor + validator; factory + registration | LowMedium (7080% AbCip reuse) | hand-rolled runtime fake (CI) + env-gated live gate (hardware; §9 checklist — string layout first) |
| **P2 — Omron FINS r/w (CJ/CS/CP)** | `IFinsTransport` codec (TCP+UDP, `0x0101`/`0x0102`, area-code table, word-swap) behind the same `IDriver` shell; memory-area TagConfig + editor fields (no browse) | Medium (net-new codec, no dep/license risk) | byte-level fake (CI) + OSS FINS sim container + **FINS validation gate** (below) |
**P2 FINS validation gate (explicit):** every FINS wire specific in this design — the area-code
table, the `0x0101`/`0x0102` command framing, the FINS/TCP node-address handshake, port 9600, and
the bit-write behavior — is **research-sourced with no in-repo evidence**. The hand-rolled FINS
framer is therefore **gated on validation against the Omron W227 FINS reference manual plus golden
request/response vectors captured from live CJ/CS/CP hardware before it is trusted**; the captured
vectors become the byte-level fake's fixtures (§9). Wire specifics stay parameterised per CPU
family (the `FinsAreaCodeTable` keying already hedged in §5.3), so a manual/hardware discrepancy is
a table correction, not a codec rewrite.
| **P3 — Sysmac tag-export importer (CIP)** | AdminUI offline importer: parse Sysmac Studio variable export → author `OmronTagDefinition`s. **Not** a live browser. | Medium | parser unit tests on real export samples |
### Top risks
1. **No free NJ/NX CIP simulator** (the headline). CIP wire correctness (string/array/UDT/
Network-Publish) is provable only on live hardware behind the env-gated `LiveIntegration` suite →
**infra-gated known limitation** until an NJ/NX is on the bench. The hand-rolled fake proves the
driver *body*, not the *wire*.
driver *body*, not the *wire* — and the NJ/NX-vs-Logix **string layout** is the likeliest wire
surprise, so it is the *first* item on the P1 live-gate checklist (§9).
2. **CIP browse is not free reuse.** libplctag's `@tags` walker returns `ErrorUnsupported` on Omron
NJ/NX; `SupportsOnlineDiscovery=false`, the universal browser doesn't apply, and browse ships as
an offline Sysmac-export importer (P3), with online browse deferred to a later research phase.
3. **FINS per-family variance** in memory-area codes + multi-word ordering — contained by the
CPU-family-parameterised `FinsAreaCodeTable` + per-tag `wordSwap`.
3. **FINS wire specifics are research-sourced, not evidence-backed.** The area codes,
`0x0101`/`0x0102` commands, FINS/TCP handshake, port 9600, and bit-write behavior have no
in-repo precedent — the hand-rolled framer is gated on the P2 validation gate (W227 manual +
golden vectors from live hardware) before it is trusted. Per-family variance in memory-area
codes + multi-word ordering is contained by the CPU-family-parameterised `FinsAreaCodeTable` +
per-tag `wordSwap`.
4. **Enum-serialization trap** — every config JSON surface must attach `JsonStringEnumConverter`, or
AdminUI-authored Omron configs fault the driver. Live-`/run` verify catches it; unit tests don't.