diff --git a/docs/drivers/AbCip.md b/docs/drivers/AbCip.md index 2f9594a2..37db1507 100644 --- a/docs/drivers/AbCip.md +++ b/docs/drivers/AbCip.md @@ -37,7 +37,7 @@ AB Legacy surface. | Capability | Implementation entry point | Notes | |------------|---------------------------|-------| -| `ITagDiscovery` | `DiscoverAsync` | Emits pre-declared tags under per-device folders; UDT tags with declared `Members` fan out into a sub-folder + one variable per member. With `EnableControllerBrowse` the `@tags` symbol table is walked into a `Discovered/` folder (system/module/routine tags filtered out). | +| `ITagDiscovery` | `DiscoverAsync` | Emits pre-declared tags under per-device folders; UDT tags with declared `Members` fan out into a sub-folder + one variable per member. With `EnableControllerBrowse` the `@tags` symbol table — plus each `Program:.@tags` listing — is walked into a `Discovered/` folder (system/module/routine tags filtered out). | | `IReadable` | `ReadAsync` → `ReadGroupAsync` / `ReadSingleAsync` | Per-tag reads; opt-in whole-UDT grouping (`EnableDeclarationOnlyUdtGrouping`) collapses N member reads into one. | | `IWritable` | `WriteAsync` | BOOL-within-DINT writes do a per-parent read-modify-write under a lock; `SafetyTag` and non-writable tags return `BadNotWritable`. | | `ISubscribable` | `SubscribeAsync` driven by the shared `PollGroupEngine` | CIP has no push model — subscriptions become polling groups. | @@ -142,13 +142,32 @@ Variables — in addition to the UDT container tag itself. Nested struct leaves that resolve to no emittable atomic leaf produce no sub-folder (lazy materialisation). - **Live verification is Emulate-tier only** (`AB_SERVER_PROFILE=emulate` + a Logix Emulate - instance): `ab_server` (the default Docker simulator) does not implement the CIP Template - Object service (class 0x6C), so the nested-id fetch cannot be exercised against it. - See `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/AbCipEmulateNestedUdtTests.cs` - for the skip-gated live-gate smoke (backlog #6). Offline unit coverage lives in - `AbCipDriverDiscoveryTests.Controller_discovered_UDT_nested_struct_expands_via_nested_template_id_fetch_no_seam` - and `CipTemplateObjectDecoderTests`. + `ab_server` (the default Docker simulator) does not implement the CIP Template Object service + (class 0x6C), so the fixture cannot exercise it; see + `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/AbCipEmulateNestedUdtTests.cs` + for the skip-gated Emulate smoke (backlog #6). +- **STRING tags and members** — a controller reports every STRING (built-in, or a user string + type such as `STR0016`) as a `LEN` DINT + `DATA` SINT[n] struct. Discovery recognises that + shape and emits **one String variable**, top-level or nested, instead of a LEN/DATA folder. +- **Hidden members are skipped** — the `ZZZZZZZZZZ…` hosts Logix packs BOOL members into and + `__…` AOI backing members are not browsed (a user name can never start with `__`). +- **Arrays** — the Symbol Object's dimension bits populate `IsArray`/`ElementCount` (multi-dim + flattened). An array of a UDT is emitted as one variable, not fanned out (`Tag.Member` names no + element); an array of STRING is a String array. +- **Program scope** — `@tags` lists each program (`Program:Main`) but none of its tags; the + enumerator reads every `Program:.@tags` listing too, so program-scoped tags appear as + `Program:.`. One unreadable program listing is skipped, not fatal. + +### Real-controller verification (2026-10-07) + +Run read-only against production CompactLogix 5069-L320ERS2 rev 34.11 presses (and a second +CompactLogix) from the Mac over VPN. Before the fixes above, every real `@udt/` buffer +decoded to `null` (the decoder assumed a layout no controller sends — the test builders shared +the assumption, the #468 trap), program-scoped tags were absent, and arrays browsed as scalars. +After them, one browse of a press took ~30 s and produced 23,412 variables covering all 80 +tags the Ignition project reads on it, with matching types. The template and symbol layouts are +pinned by bytes captured off those controllers: `CipTemplateObjectWireCaptureTests`, +`CipSymbolObjectRealControllerCaptureTests`, `AbCipProgramScopeEnumerationTests`. ### Bare-container reads @@ -170,4 +189,8 @@ 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. +- **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.