From 5dd1b987b92e5077523dc9f264cbf4e94f898432 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 7 Oct 2026 16:58:54 -0400 Subject: [PATCH] docs(plans): native AB transport tasks.json status sync --- docs/plans/2026-10-07-native-ab-transport.md.tasks.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/plans/2026-10-07-native-ab-transport.md.tasks.json b/docs/plans/2026-10-07-native-ab-transport.md.tasks.json index 9b475e6e..391ebdfd 100644 --- a/docs/plans/2026-10-07-native-ab-transport.md.tasks.json +++ b/docs/plans/2026-10-07-native-ab-transport.md.tasks.json @@ -106,7 +106,7 @@ { "id": 12, "subject": "Task 12: `NativeAbCipTagRuntime` + factory", - "status": "pending", + "status": "completed", "description": "**Goal:** A native `IAbCipTagRuntime` that leases the device connection from `DeviceSessionPool.Shared`, reads and writes through `LogixTagClient`, decodes the managed buffer with the same semantics as `LibplctagTagRuntime`, and adds a CIP type check.\n\n**Files:**\n- Create: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/NativeAbCipTagRuntime.cs`\n- Create: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/NativeAbCipTagFactory.cs`\n- Create: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/NativeAbCipDevice.cs` (maps `AbCipTagCreateParams` \u2192 `DeviceKey` + `ConnectionOptions`; shared with Task 13)\n- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs` (add trailing optionals `AbCipDataType? AuthoredType = null, bool UseConnectedMessaging = true` to `AbCipTagCreateParams`)\n- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverProbe.cs` (pass `UseConnectedMessaging: profile.SupportsConnectedMessaging`)\n- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs` (`EnsureTagRuntimeAsync` passes `AuthoredType: def.DataType`; `DeviceState`'s create-params builder passes `UseConnectedMessaging: Profile.SupportsConnectedMessaging`)\n- Create: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/NativeAbCipTagRuntimeTests.cs`\n- Modify: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests.csproj` (add a `ProjectReference` to Ab.Transport and ``.; source-linking compiles the fake into this assembly, so it stays `internal`)\n\n**Acceptance Criteria:**\n- [ ] `NativeAbCipDevice.Key(p)` = `new DeviceKey(p.Gateway, p.Port > 0 ? p.Port : 44818, p.CipPath, p.LibplctagPlcAttribute)`.\n- [ ] `NativeAbCipDevice.Options(p)`:\n - `Timeout = p.Timeout`, `ConnectionSize = p.ConnectionSize`, `AllowPacking = p.AllowPacking`.\n - **`Routing`:** if `p.UseConnectedMessaging` is false, use `UnconnectedSend` when `p.CipPath` is non-empty and `Direct` when it is empty. Otherwise use `Connected`.\n - `UseConnectedMessaging` is a new trailing optional `bool UseConnectedMessaging = true` on `AbCipTagCreateParams`. It is set at every construction site from `AbCipPlcFamilyProfile.SupportsConnectedMessaging`, so the family profile stays the single authority and string-matching `\"micro800\"` is not needed (review finding 18). The sites are `AbCipDriver` `DeviceState` (around `AbCipDriver.cs:1690-1700`, where `AllowPacking`/`ConnectionSize` are already merged) and `AbCipDriverProbe`; find any others with `grep -rn \"new AbCipTagCreateParams\" src`.\n - Cross-check the result against the Task 4 \"Routing observed\" table for controllogix and micro800, and record any mismatch in the test comment.\n- [ ] **Initialization and reads:**\n - `InitializeAsync` acquires the lease from `DeviceSessionPool.Shared` and builds a per-runtime `LogixTagClient` over **the lease's shared `Scheduler` and `Channel`**. It must never construct its own `RequestScheduler`: that would defeat MSP coalescing across tags (B1). It encodes the path once, splitting off a trailing bit selector with `CipPath.SplitBitSelector`; that is stored as the runtime's implicit bit.\n - It performs **one read** to prove the tag exists. That matches libplctag, whose `InitializeAsync` errors for an unknown tag; the driver's 05/STAB-8 logic depends on create throwing `NotFound`.\n - `ReadAsync` reads `p.ElementCount` elements, stores the data, `TypeCode` and `StructHandle`, and sets status `Ok`. On `AbTransportException` it stores the exception's status **and rethrows**, matching libplctag's exception-on-failure behaviour. `GetSize()` returns the data length.\n- [ ] **Decode** is byte-identical to `LibplctagTagRuntime` for every `AbCipDataType` (Bool/SInt/USInt/Int/UInt/DInt/UDInt/LInt/ULInt/Real/LReal/String/Dt). Specifically:\n - Bool with a bit index \u21d2 bit `n` of the little-endian buffer.\n - Bool without \u21d2 `byte[0] != 0`.\n - String \u21d2 Logix STRING `u32 LEN` + `DATA`, ASCII, `LEN` clamped to the data length \u2212 4.\n - `DecodeArray` uses stride = `data.Length / count` when it divides evenly, else the static width (the same `ResolveArrayStride` rule; call `LibplctagTagRuntime.ResolveArrayStride` with `elementSize: null` to share it).\n - Bool arrays are bit-packed (bit `i` of the buffer).\n- [ ] **Type check:** after a read, if `TypeCode` is atomic and differs from the authored type's Logix code, `GetStatus()` returns `(int)AbStatus.TypeMismatch` and `Decode*` returns null. The mapping is `AbCipDataType` \u2192 `LogixTypeCode`, with `Dt \u2192 DInt`; `String`/`Structure` are exempt. Exemptions:\n - (a) A BOOL read with a bit index against a DINT host is **not** a mismatch.\n - (b) **Authored `Bool` with returned `0xD3`** (the Logix packed BOOL-array type) is **not** a mismatch (review finding 11).\n - Tests cover both exemptions, plus a positive mismatch.\n- [ ] **Encode/write:**\n - `EncodeValue` fills a managed write buffer with the same encodings as `LibplctagTagRuntime.EncodeValue`, including the same `NotSupportedException` cases.\n - `WriteAsync` writes with type = the last read `TypeCode` if known, else the authored type's code.\n - For String, it uses `Struct` + the last `StructHandle`. If none is known it performs a read first; that read happens inside `WriteAsync`, under the driver's existing per-runtime lock.\n - The STRING write payload is `u32 len` + DATA padded to the capacity learned from the last read's data length \u2212 4.\n- [ ] `Dispose` releases the lease, waiting for at most 2 s (`lease.DisposeAsync().AsTask().Wait(TimeSpan.FromSeconds(2))`). It must not fire and forget: an immediate re-init would otherwise open a second session to the press while the old Forward Close is still in flight (review finding 22). It never disposes the shared scheduler; the pool does that on the last release.\n- [ ] `NativeAbCipTagFactory : IAbCipTagFactory` creates `NativeAbCipTagRuntime`.\n- [ ] Test seam: `internal static NativeAbCipTagRuntime ForTest(ICipChannel channel, AbCipTagCreateParams p)` builds a runtime over the given channel with a private `RequestScheduler(channel, p.AllowPacking)` (test-only; no pool lease). Production code always goes through the factory and `DeviceSessionPool.Shared`.\n- [ ] **Unit tests** (against `FakeCipChannel` via `ForTest`):\n - decode parity for each type, using a buffer captured from `LibplctagTagRuntime` semantics (hard-coded bytes);\n - the TypeMismatch case (REAL returned for a DInt-authored tag);\n - an unknown tag \u21d2 `InitializeAsync` throws `AbTransportException(NotFound)`;\n - a STRING write sends `Struct` + the handle learned from the prior read;\n - a 1-element array is read as an array (`IsArray`).\n\n**Verify:** `dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests --filter NativeAbCipTagRuntime` \u2192 pass.\n\nSteps + code: docs/plans/2026-10-07-native-ab-transport.md \u00a7 Task 12.\n\n```json:metadata\n{\"files\": [\"ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests\", \"src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs\", \"src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverProbe.cs\", \"src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs\", \"src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/NativeAbCipDevice.cs\", \"src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/NativeAbCipTagFactory.cs\", \"src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/NativeAbCipTagRuntime.cs\", \"tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/NativeAbCipTagRuntimeTests.cs\", \"tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests.csproj\"], \"verifyCommand\": \"dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests --filter NativeAbCipTagRuntime\", \"modelTier\": \"frontier\", \"model\": \"opus\", \"acceptanceCriteriaCount\": 10}\n```", "blockedBy": [ 2, @@ -126,7 +126,7 @@ { "id": 14, "subject": "Task 14: `NativeAbLegacyTagRuntime` + factory", - "status": "pending", + "status": "completed", "description": "**Goal:** A native `IAbLegacyTagRuntime` over PCCC (Task 8) carried in Execute PCCC through a `DeviceSessionPool` lease.\n\n**Files:**\n- Create: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/NativeAbLegacyTagRuntime.cs`\n- Create: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/NativeAbLegacyTagFactory.cs`\n- Create: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/NativeAbLegacyTagRuntimeTests.cs`\n- Modify: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests.csproj` (reference Ab.Transport + link `FakeCipChannel.cs`, as in Task 12)\n\n**Acceptance Criteria:**\n- [ ] **Connection:**\n - `DeviceKey` is built from `AbLegacyTagCreateParams` the same way as Task 12 (default port 44818).\n - **`Routing` comes from the Task 4 \"Routing observed\" table (B3), per family:**\n - Expected: `slc500`/`micrologix`/`plc5` \u21d2 **`Direct`** (libplctag sends Execute PCCC bare in SendRRData even though the driver forces `EffectiveCipPath = \"1,0\"`, `AbLegacyDriver.cs:864`); `logixpccc` \u21d2 **`Connected`**.\n - **If the capture shows otherwise, the capture wins.** Encode the choice in one `static CipRouting RoutingFor(string plcAttribute)` with a comment citing the capture file, and pin it with a test per family.\n - `ConnectionSize = 504`, `AllowPacking = false` (PCCC requests are never MSP-packed).\n - Send through the lease's shared `Scheduler`, never a private one (B1).\n- [ ] **Address handling:**\n - `AbLegacyAddress.TryParse(p.TagName)` \u21d2 `PcccAddress(FileLetter, FileNumber, WordNumber, sub)`. The sub-element comes from `ResolveSubElement`. A `BitIndex` is kept for decode only, because reads are word reads.\n - Byte counts per element: N/A/B/I/O/S = 2, F/L = 4, T/C/R = 6 when no sub-element (the whole element), 2 when a word sub-element is chosen, ST = 84.\n - Element count = `p.ElementCount`.\n- [ ] **Read and write:**\n - `ReadAsync` dispatches by `plc` attribute: `slc500`/`micrologix`/`logixpccc` \u21d2 `SlcTypedRead`; `plc5` \u21d2 `Plc5TypedRead`, or `AbTransportException(Unsupported)` when `PcccServices.Plc5Verified` is false (Task 8).\n - The TNS increments per request.\n - **Chunking (review finding 12):** a read or write whose byte count exceeds `PcccServices.MaxDataBytes` (236) is split into consecutive element ranges of at most 236 bytes (118 words, or 59 longs/floats). The element number advances per chunk and the data is concatenated in order. A failure in any chunk fails the whole operation with that chunk's status; a partial write is reported as failed, never as Good. Test a 256-word array read (three chunks: 118 + 118 + 20) and a 130-word write (two chunks).\n - On failure it stores the status and rethrows.\n - `InitializeAsync` performs one read, for the same `NotFound`-on-create reason as Task 12.\n- [ ] **Decode** is identical to `LibplctagLegacyTagRuntime`:\n - Bit with index \u21d2 bit of the LE word (`/N` up to 15, or up to 31 for L).\n - Bit without \u21d2 `(word & 1) != 0`.\n - Int/AnalogInt \u21d2 `short`\u2192`int`; Long \u21d2 `int`; Float \u21d2 `float`.\n - String \u21d2 the SLC ST layout: `u16 LEN` + 82 data bytes **with each byte pair swapped**. \u26a0\ufe0f Verify this byte order against `slc500` ST data in ab_server. If ab_server does not seed ST, test against a hand-built buffer and flag it in Task 20 docs as unverified.\n - Timer/Counter/Control element without a sub-element \u21d2 `GetInt32(0)` parity (the first 4 bytes, LE).\n - `DecodeArray` uses the offsets documented in `LibplctagLegacyTagRuntime.DecodeArray`.\n- [ ] **Encode** follows `LibplctagLegacyTagRuntime.EncodeValue` case by case, with the same `NotSupportedException` for bit-with-index. `WriteAsync` \u21d2 `SlcTypedWrite` (or PLC-5 / Unsupported).\n- [ ] **Unit tests:**\n - N7:0 read decode;\n - F8 read;\n - B3:0/5 decode;\n - an N7 array of 5;\n - T4:0.ACC sub-element addressing (bytes on the wire);\n - STS 0x20 \u21d2 `NotFound` on initialize;\n - a write of N7:0 = 42 produces the expected PCCC bytes.\n\n**Verify:** `dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests --filter NativeAbLegacyTagRuntime` \u2192 pass.\n\nSteps + code: docs/plans/2026-10-07-native-ab-transport.md \u00a7 Task 14.\n\n```json:metadata\n{\"files\": [\"src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/NativeAbLegacyTagFactory.cs\", \"src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/NativeAbLegacyTagRuntime.cs\", \"tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/NativeAbLegacyTagRuntimeTests.cs\", \"tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests.csproj\"], \"verifyCommand\": \"dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests --filter NativeAbLegacyTagRuntime\", \"modelTier\": \"standard\", \"model\": \"sonnet\", \"acceptanceCriteriaCount\": 6}\n```", "blockedBy": [ 3, @@ -147,7 +147,7 @@ { "id": 16, "subject": "Task 16: AbLegacy `Transport` switch (options, factory, prob", - "status": "pending", + "status": "completed", "description": "**Goal:** Do the same as Task 15 for AbLegacy.\n\n**Files:**\n- Create: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyTransport.cs` (`public enum AbLegacyTransport { Libplctag, Native }`)\n- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyDriverOptions.cs` (`Transport` property, default `Libplctag`)\n- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverFactoryExtensions.cs` (DTO + parse)\n- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs:65-71` (`_tagFactory = tagFactory ?? (options.Transport == AbLegacyTransport.Native ? new NativeAbLegacyTagFactory() : new LibplctagLegacyTagFactory());`)\n- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverProbe.cs`\n- Modify: `src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/` (`--transport`)\n- Create: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyTransportSwitchTests.cs`\n- Modify: `tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Tests/`\n\n**Acceptance Criteria:** the same five criteria as Task 15, applied to AbLegacy types:\n- [ ] `\"transport\": \"Native\"` and `\"native\"` select the native factory, absent selects libplctag, and a bad value fails the way an existing bad enum field does.\n- [ ] An injected factory wins over the option.\n- [ ] The probe with Native uses the native path; prove it with the unreachable-endpoint message test.\n- [ ] The CLI `--transport native` reaches the options.\n- [ ] A reinit with a changed transport keeps the constructed transport (`AbLegacyDriver` reinit has the same Shutdown+Initialize shape), pinned by the same kind of test and documentation as Task 15.\n\n**Verify:** `dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests --filter AbLegacyTransportSwitch && dotnet test tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Tests` \u2192 pass.\n\nSteps + code: docs/plans/2026-10-07-native-ab-transport.md \u00a7 Task 16.\n\n```json:metadata\n{\"files\": [\"src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/\", \"src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyDriverOptions.cs\", \"src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyTransport.cs\", \"src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs:65-71\", \"src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverFactoryExtensions.cs\", \"src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverProbe.cs\", \"tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Tests/\", \"tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyTransportSwitchTests.cs\"], \"verifyCommand\": \"dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests --filter AbLegacyTransportSwitch && dotnet test tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Tests\", \"modelTier\": \"standard\", \"model\": \"sonnet\", \"acceptanceCriteriaCount\": 6}\n```", "blockedBy": [ 14