test(abcip): Emulate-tier nested-UDT live-gate smoke + docs (backlog #6)

Add AbCipEmulateNestedUdtTests (skip-gated, AB_SERVER_PROFILE=emulate) to close
the live-gate gap for nested-struct UDT discovery via CIP Template Object (class 0x6C)
threaded by commits 3d8ce4e8/d203f31c. Compiles + skips cleanly against ab_server
(no CIP Template Object service). Update docs/drivers/AbCip.md nested-struct section
to record the shipped decode path, the Emulate-only live-gate, and offline unit coverage.
This commit is contained in:
Joseph Doherty
2026-06-18 12:40:04 -04:00
parent 70aad3ef48
commit c8ab8fc348
2 changed files with 149 additions and 7 deletions
+17 -7
View File
@@ -132,13 +132,23 @@ Variables — in addition to the UDT container tag itself.
separate Variable node under the UDT's `Discovered/` sub-folder, addressable by its member
path (e.g. `MyUdt.Temperature`, `MyUdt.Flags`). Top-level atomic member discovery is
**functional in production**.
- **Nested struct members** — when a UDT member is itself a struct, the driver walks into it up
to a **depth cap of 8 levels**. Nested-struct expansion is a **documented deferral in
production**: the Template Object member block carries no nested template ID, so the
sub-shape cannot be re-fetched from the controller at discovery time. Nested struct leaves are
never mis-emitted — they are simply dropped (the parent member is omitted from discovery if it
is not atomic and its sub-shape is unavailable). Use pre-declared `Members` for nested structs
that must be individually addressed.
- **Nested struct members** — **shipped** (commits 3d8ce4e8 / d203f31c). When a UDT member is
itself a struct, the Template Object member block carries the nested UDT's template instance ID
in the low 12 bits of the member `info` field (struct flag `0x8000` set). The driver captures
this as `AbCipUdtMember.NestedTemplateId` and threads it into a second `FetchUdtShapeAsync`
call (`@udt/{id}`) so the nested struct's atomic leaves are expanded into dot-joined addressable
paths (e.g. `Motor1.Status.Code`, `Motor1.Status.Running`). Recursion is bounded by a
**depth cap of 8 levels**; members at or beyond the cap are dropped rather than emitted.
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`.
### Bare-container reads