diff --git a/CLAUDE.md b/CLAUDE.md index a11fb574..74e3379b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -973,6 +973,18 @@ docker-dev. The rationale for every decision above is in Dev/test LDAP is the **shared GLAuth** running on the Linux Docker host at `10.100.0.35:3893` (baseDN `dc=zb,dc=local`, plaintext/`Transport=None`). It is managed via `scadaproj/infra/glauth/` (source of truth + deploy runbook). Single bind account `cn=serviceaccount,dc=zb,dc=local` / `serviceaccount123`; all test users password `password`. The docker-dev compose binds this shared instance directly — `DevStubMode` is no longer used. The per-VM NSSM GLAuth at `C:\publish\glauth\` and the old base DNs `dc=lmxopcua,dc=local` / `dc=otopcua,dc=local` are obsolete. (The integration-test harness under `tests/.../Host.IntegrationTests/` runs its own ephemeral **GLAuth** container on port 3894 — seeded by `tests/.../Host.IntegrationTests/glauth/config.toml` with dev users alice/bob + the `cn=serviceaccount` bind account — for the opt-in `OTOPCUA_HARNESS_USE_LDAP=1` real-bind mode; distinct from the shared dev GLAuth on 3893. It replaced the retired `bitnami/openldap:2.6` in PR #451, 2026-07-15.) +## AB drivers: native transport (`Transport`) + +AbCip and AbLegacy have a second, managed wire transport (`Driver.Ab.Transport`) beside libplctag, chosen by +the driver-config key `transport` = `Libplctag` (default) | `Native` (string; takes effect when the driver is +recreated by a deploy; AdminUI "Transport" dropdown; CLIs `--transport`). **Opt-in:** the default stays +`Libplctag` until the real-press read-only gate (Task 21) runs and the default is flipped. Native writes are +verified against `ab_server` only. Client-visible: native reports `BadTypeMismatch` where libplctag returned +Good garbage for a same-width mis-authored type (DINT authored, REAL returned). libplctag is confined to +`Libplctag*.cs` adapters by `LibplctagConfinementGuardTests`; do not reference it elsewhere. See +`docs/drivers/AbCip.md` §Transport, `docs/drivers/AbLegacy.md`, and +`docs/plans/2026-10-07-native-ab-transport-gate-results.md`. + ## Library Preferences - **Logging**: Serilog with rolling daily file sink diff --git a/docs/drivers/AbCip.md b/docs/drivers/AbCip.md index 37db1507..156837fd 100644 --- a/docs/drivers/AbCip.md +++ b/docs/drivers/AbCip.md @@ -76,12 +76,78 @@ the driver's `DriverConfig` JSON. Key fields: - **`Devices`** — one `AbCipDeviceOptions` per PLC (`HostAddress`, `PlcFamily`, optional `DeviceName`, per-device `AllowPacking` / `ConnectionSize` overrides). - **`Tags`** — pre-declared `AbCipTagDefinition` list; `Members` for UDT fan-out, `SafetyTag` for GuardLogix safety-partition tags. - **`Probe`** — connectivity-probe `Enabled` / `Interval` / `Timeout` / `ProbeTagPath`. +- **`Transport`** — `Libplctag` (default) or `Native`; see [Transport](#transport-transport-option). - **Discovery** — `EnableControllerBrowse` (`@tags` walk) and `EnableDeclarationOnlyUdtGrouping` (whole-UDT read fast path). - **Alarms** — `EnableAlarmProjection` + `AlarmPollInterval` for the ALMD projection. Full per-field descriptions live in `AbCipDriverOptions.cs`. The JSON skeleton is reproduced in [docs/v2/driver-specs.md §3](../v2/driver-specs.md). +## Transport (`transport` option) + +The driver has two interchangeable wire transports, selected by one `DriverConfig` key: + +| Key | Values | Default | Serialization | +|---|---|---|---| +| `transport` | `Libplctag` \| `Native` | `Libplctag` | string, case-insensitive on read, written as `"Libplctag"` / `"Native"` | + +- **`Libplctag`** (default) drives the libplctag native library, as before. +- **`Native`** is a managed in-process EtherNet/IP + CIP client (`Driver.Ab.Transport`). It is **opt-in**: + the default stays `Libplctag` until the real-press read-only gate has run and the default is flipped. +- A bad value fails like a bad `PlcFamily`; the Test Connect probe reports "Config JSON is invalid". +- A change takes effect when the driver instance is **recreated**. A deploy that changes `DriverConfig` + does this (`DriverHostActor` respawns the child, #516). `ReinitializeAsync` alone keeps the transport + it was constructed with. +- AdminUI: the **Transport** dropdown in the advanced section of the AbCip driver form ("libplctag + (default)" / "Native (managed)"). Saving another field preserves it. +- The driver logs `transport=` at Information on every init, including reconnect re-inits. AbCip + drivers now receive the host logger (they previously ran on a null logger, so none of their warnings + reached the node log); expect previously invisible warnings, e.g. a probe enabled with no probe tag path. +- CLI: `--transport native|libplctag` (default `libplctag`). +- libplctag is confined to `Libplctag*.cs` adapters, enforced by a source-scan guard + (`LibplctagConfinementGuardTests`), so removing it later is mechanical. + +## Native transport behaviour + +- **One CIP connection and one request scheduler per device** (`DeviceSessionPool`), shared by reads, + writes, browse and template reads. The Multiple Service Packet scheduler coalesces requests with one + request in flight; large reads (listing pages, Read Template) are sent unpacked and exclusive. +- **Forward Open throttle:** at most one attempt in flight, at least 1 s between starts, bounded by the + open budget. +- `ConnectionSize` / `Timeout` are honoured per pooled connection, and the **first** driver/device to open + a given device key wins. +- **Routing:** Micro800 is connected with an empty route regardless of `UseConnectedMessaging`. + SLC500/MicroLogix/PLC-5 over this driver are direct. `logixpccc` uses UnconnectedSend; its SLC framing is + **unverified**. +- ⚠️ **Type check (client-visible change).** The native path compares the type code the controller returns + with the authored `DataType` and reports **`BadTypeMismatch`** on disagreement. libplctag never checks it, + so a same-width mis-authored tag read as **Good garbage**: authoring a tag as `DINT` while the controller + holds a `REAL` returns the REAL's bits as an integer with a Good status under libplctag, and + `BadTypeMismatch` natively. That is the fix working, but a consumer will see Bad quality on tags that + previously looked healthy: correct the authored type. Same-width signed/unsigned pairs are exempt, and + writes are gated on a mismatch too. +- Malformed tag names put the device into backoff, as under libplctag (#570). STRING capacity includes the + alignment pad (84, not 82) (#570). +- Status-bit sub-element writes (e.g. `T4:0/DN`) are Unsupported natively (they would clobber the control + word); nothing goes on the wire. Sub-element arrays are Unsupported natively. + +## Known gaps + +- ⚠️ **Native writes are verified against `ab_server` only (simulator-only), never a real controller.** The + real-press gate (Task 21) is **read-only** and **has not run yet**; this page makes no real-controller + claim for the native transport. Results land in + [native-ab-transport-gate-results](../plans/2026-10-07-native-ab-transport-gate-results.md). +- **libplctag cannot be deleted** until a bench controller allows write verification. +- Wide-struct fragmented writes (8-byte alignment) are unit-tested only, never proven on a controller (#567). +- Native browse: listing pages are unpaced, and template reads outside an enumeration open a connection per + call (#572). +- Micro800, routed and `logixpccc` paths are covered by `ab_server` only. The Micro800 profile passes + natively; the planned default flip excludes `logixpccc`. +- Follow-up issues for this workstream: #564 to #577. +- Fixed along the way (pre-existing on master): `ab://host:44818/` with an empty CIP path (Micro800, + unrouted MicroLogix) failed on libplctag with `ErrorBadData` because the adapter sent `Path=""`; it now + sends no path. + ## Alarm Projection `IAlarmSource` is served by `AbCipAlarmProjection`, which polls each subscribed @@ -190,7 +256,7 @@ UDT-typed tags found in the walk. - **Native heap is invisible to the GC.** libplctag's native `Tag` heap does not show up in CLR allocation figures. Watch whole-process RSS, and use `ReinitializeAsync` (tears down + re-creates every device's libplctag handles) as the remediation for native-heap growth. (There is no per-driver footprint number to consult: `IDriver.GetMemoryFootprint()` was deleted as unconsumed in Gitea #525.) - **Handle eviction on failure** — a non-zero libplctag status or a transport exception evicts the cached tag runtime so the next read/write re-creates a fresh handle, mirroring the probe loop's recreate-on-failure behaviour. - **Read statuses carry the libplctag cause** — a tag the controller does not have reads `BadNodeIdUnknown` (not `BadCommunicationError`), and it does **not** open the device's connect-backoff window (the controller answered; before this, one typo throttled every sibling tag on the device). A genuine connection failure still opens it. -- **Width check on scalar reads** — libplctag never checks the CIP type the controller returns. A scalar atomic read whose buffer is not the authored type's width (e.g. a REAL authored as INT — the legacy Kepware export does exactly this for some press tags) reads `BadTypeMismatch` instead of Good garbage. Same-width mistakes (DINT vs REAL) are still undetectable. +- **Width check on scalar reads** — libplctag never checks the CIP type the controller returns. A scalar atomic read whose buffer is not the authored type's width (e.g. a REAL authored as INT — the legacy Kepware export does exactly this for some press tags) reads `BadTypeMismatch` instead of Good garbage. Same-width mistakes (DINT vs REAL) are undetectable under libplctag; the `Native` transport reports them as `BadTypeMismatch` (see [Native transport behaviour](#native-transport-behaviour)). - **Module-defined I/O tags** — `Local:1:I.Data`, `ABB_OmniCore:I.ConnectionFaulted` and similar colon-bearing base names are valid tag paths (controller scope only). - **User string arrays** — `STR0016[N]` and similar arrays stride by the actual element size (read buffer ÷ count); libplctag.NET 1.5.2 leaves `ElementSize` unset after a read. - **Declaration-only UDT grouping is a footgun unless verified** — only enable `EnableDeclarationOnlyUdtGrouping` when every UDT's member declaration order has been hand-verified against the controller's compiled layout. diff --git a/docs/drivers/AbLegacy-Test-Fixture.md b/docs/drivers/AbLegacy-Test-Fixture.md index f537020b..2c7f459a 100644 --- a/docs/drivers/AbLegacy-Test-Fixture.md +++ b/docs/drivers/AbLegacy-Test-Fixture.md @@ -32,6 +32,17 @@ still the primary coverage. All tests tagged `[Trait("Category", "Unit")]`. The driver accepts `IAbLegacyTagFactory` via ctor DI; every test supplies a `FakeAbLegacyTag`. +## Per-transport suites and capture recipe + +The `AbLegacy.IntegrationTests` suites now run **once per transport** (`TransportMatrix`: `Libplctag` and +`Native`). Set `AB_LEGACY_COMPOSE_PROFILE` to the running profile. On the 2026-10-07 run native passed +everything libplctag passed on `slc500`, `micrologix` and `plc5` (the PLC-5 test reads N7 only, so the PLC-5 +refusal paths and status-bit writes are not exercised by it). Native PCCC is **simulator-only**. Write-capable +tests (the SLC500 N7:5 round trip) require `AB_SERVER_ALLOW_WRITES=1`. Golden frames: +`scripts/ab-capture/capture.sh`, fixtures under +`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests/Captures/`. Evidence: +[native-ab-transport-gate-results](../plans/2026-10-07-native-ab-transport-gate-results.md). + ## What it actually covers (unit only) - `AbLegacyAddressTests` — PCCC address parsing for SLC / MicroLogix / PLC-5 diff --git a/docs/drivers/AbLegacy.md b/docs/drivers/AbLegacy.md index 7cbcdfe6..b6afa87b 100644 --- a/docs/drivers/AbLegacy.md +++ b/docs/drivers/AbLegacy.md @@ -93,10 +93,39 @@ binds from the driver's `DriverConfig` JSON: - **`Devices`** — one `AbLegacyDeviceOptions` per PLC (`HostAddress`, `PlcFamily`, optional `DeviceName`). - **`Tags`** — pre-declared `AbLegacyTagDefinition` list (`Name`, `DeviceHostAddress`, `Address`, `DataType`, `Writable`, `WriteIdempotent`). - **`Probe`** — connectivity-probe `Enabled` / `Interval` / `Timeout` / `ProbeAddress`. +- **`Transport`** — `Libplctag` (default) or `Native`; see [Transport](#transport-transport-option). Full per-field descriptions live in the contracts assembly. The JSON skeleton is reproduced in [docs/v2/driver-specs.md §4](../v2/driver-specs.md). +## Transport (`transport` option) + +Same switch as AB CIP: `transport` = `Libplctag` (default) \| `Native`, a string (case-insensitive on read, +written as `"Libplctag"` / `"Native"`). `Native` is a managed PCCC-over-EtherNet/IP client +(`Driver.Ab.Transport`) and is **opt-in**; the default stays `Libplctag` pending the real-press gate and the +default flip. A change takes effect when the driver instance is recreated (a deploy that changes +`DriverConfig`); the AdminUI form has a **Transport** dropdown in the advanced section, and the driver logs +`transport=` on every init. CLI: `--transport native|libplctag`. See +[AbCip.md §Transport](AbCip.md#transport-transport-option) for the shared behaviour (one pooled connection +per device, Forward Open throttle, routing). + +Native routing: SLC500/MicroLogix/PLC-5 are direct; `logixpccc` uses UnconnectedSend and its SLC framing is +**unverified**. + +### Native known gaps + +- ⚠️ **Simulator-only.** PCCC reads and writes are verified against `ab_server` only. No real SLC/MicroLogix/ + PLC-5 has been exercised, and the real-press gate (Task 21) is read-only and has not run yet. +- **PLC-5 native is N-file reads only.** Every other file type and all PLC-5 writes are refused, currently + surfacing as `BadCommunicationError` rather than `BadNotSupported` (#571). The planned default flip excludes + PLC-5 and `logixpccc`. +- Status-bit sub-element writes (e.g. `T4:0/DN`, `C5:0/DN`) are Unsupported natively (they would clobber the + control word); nothing goes on the wire. Sub-element arrays are Unsupported natively. L-file bit indexes + above 15 are allowed. +- **ST (string) file layout is uncaptured**, so the string byte order is unverified natively. +- Capture recipe for golden frames: `scripts/ab-capture/capture.sh` (see + [AbLegacy-Test-Fixture.md](AbLegacy-Test-Fixture.md)). Follow-up issues: #564 to #577. + ## Testing - **Unit tests** — `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/` cover the driver, the PCCC address parser, and the host-address parser via fake tag runtimes. diff --git a/docs/drivers/AbServer-Test-Fixture.md b/docs/drivers/AbServer-Test-Fixture.md index 55c2f476..52aae1b5 100644 --- a/docs/drivers/AbServer-Test-Fixture.md +++ b/docs/drivers/AbServer-Test-Fixture.md @@ -33,6 +33,28 @@ quirk. UDT / alarm / quirk behavior is verified only by unit tests with - **Endpoint override**: `AB_SERVER_ENDPOINT=host:port` points the fixture at a real PLC instead of the local container. +## Per-transport suites and capture recipe + +Every integration test in `AbCip.IntegrationTests` now runs **once per transport** (`TransportMatrix`: +`Libplctag` and `Native`). Run one compose profile at a time and name it with `AB_SERVER_COMPOSE_PROFILE` +(all profiles bind `:44818`, so set it to the running profile). On the 2026-10-07 run native passed everything +libplctag passed on all seven profiles; on `micro800` libplctag failed on the empty-path defect described in +[AbCip.md](AbCip.md#known-gaps) and native passed. Evidence: +[native-ab-transport-gate-results](../plans/2026-10-07-native-ab-transport-gate-results.md). + +- Write-capable tests (the Logix Emulate ALMD test) require `AB_SERVER_ALLOW_WRITES=1` and refuse while + `AB_DIFF_ENDPOINT` is set. +- **Differential harness** (`AbCip.IntegrationTests/Differential`, `LiveIntegration`) compares libplctag and + native on browse, template and read parity against one endpoint. It is read-only by construction, aborts on + the first connection failure, never retries, and caps the native phase at one Forward Open. Env: + `AB_DIFF_ENDPOINT`, `AB_DIFF_PATH`, `AB_DIFF_FAMILY`, `AB_DIFF_TAGS`, `AB_DIFF_MAX_TAGS` (at most 500), + `AB_DIFF_MAX_TEMPLATES`, `AB_DIFF_PACE_MS` (default 20), `AB_DIFF_COUNT_OPENS`. Always run with + `--filter "FullyQualifiedName~Differential"` and `AB_SERVER_ENDPOINT=127.0.0.1:1`. +- ab_server serves no UDTs and refuses 1-byte writes, so template parity is vacuous there and BOOL/SINT + parity is weak evidence. Native writes remain simulator-only. +- **Golden-frame capture:** `scripts/ab-capture/capture.sh` (capture client in `scripts/ab-capture/client`); + fixtures live in `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests/Captures/` with a README. + ## What it actually covers - Read path: driver → libplctag → CIP-over-EtherNet/IP → simulator → back.