# AbCip Driver — Implementation Plan > Source of gap analysis: [featuregaps.md → AbCip](../featuregaps.md#abcip-allen-bradley-ethernetip--logix) > > This plan covers the **Build = Yes** items only. Skip-rated gaps are listed at the bottom for traceability. ## Summary This plan closes the 16 Build-rated AbCip gaps in five phases ordered to ship correctness fixes first, then engineering workflow, then performance, then operability, and finally redundancy. Phase 1 lands the data-type fidelity work (LINT/ULINT, native STRINGnn, array slicing, write-multi packing) that today silently truncates 64-bit values and serialises adjacent reads into N round-trips. Phase 2 introduces the offline tag-import workflow (L5K/L5X + CSV) that Studio 5000 shops require before they will switch off Kepware. Phase 3 exposes the performance levers commercial drivers ship as field knobs — symbolic vs logical addressing, configurable Connection Size, and the logical-blocking / logical-non-blocking strategy selector. Phase 4 surfaces per-tag scan rates, write deadband, online tag-DB refresh trigger, and the diagnostic system tags an HMI dashboard expects. Phase 5 adds HSBY paired-IP failover for continuous-process plants. Headline outcome: parity with Kepware's Logix Database Settings and TOP Server's protocol-mode picker, with measurable throughput wins (3-5x on dense rigs via logical addressing, single-PDU reads on contiguous arrays, single-PDU writes on multi-tag recipe pushes). ## Phased delivery ### Phase 1 — Data-type correctness (4 PRs) Goal: stop silently losing data. None of the items in this phase are user-visible features — they are correctness fixes against existing capability surfaces. #### PR 1.1 — LINT / ULINT 64-bit fidelity - **Scope**: replace the truncating `Int32` widening at `AbCipDataType.cs:53` with `Int64` routing across decode + encode + the `DriverDataType` map. Includes `DT` (epoch-millis on Logix v32+ surfaces as LINT, not DINT — verify against `LibplctagTagRuntime.cs:53` before reusing the same code-path). - **Files**: `AbCipDataType.cs` (mapping), `LibplctagTagRuntime.cs` (already calls `_tag.GetInt64` / `SetInt64`, so the runtime is correct — the gap is the surface enum flattening into `Int32`), `Core.Abstractions/DriverDataType.cs` may need an `Int64` / `UInt64` member if not already present. - **Test approach**: unit (xUnit + Shouldly) with a fake `IAbCipTagRuntime` that returns `long.MaxValue` on `DecodeValueAt(LInt, ...)`; assert the snapshot value round-trips through the read path without truncation. Integration test against pymodbus is N/A — needs a live Logix or a libplctag mock-server fixture; keep this unit-only and rely on smoke testing on the dev box with a real ControlLogix. - **Effort**: S - **Dependencies**: confirm `DriverDataType.Int64` exists; if not, that is a Core change shared with the Modbus TODO at `AbCipDataType.cs:53`. - **Docs / fixture / e2e**: appends a Logix-types row to the type-mapping table in `docs/Driver.AbCip.Cli.md` (CLI gains `--type LInt` / `--type ULInt`); extends `docs/drivers/AbServer-Test-Fixture.md` §"What it actually covers" to list `LINT` once ab_server is reseeded with a `TestLINT:LINT[1]` tag; updates the `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/docker-compose.yml` ControlLogix profile to seed `TestLINT`; adds a 64-bit assertion case in `AbCipReadSmokeTests`; extends `scripts/e2e/test-abcip.ps1` with an LInt loopback assertion (and a matching seeded `TestLINT` in `scripts/smoke/seed-abcip-smoke.sql`). #### PR 1.2 — Native STRING / STRINGnn variant decoding - **Scope**: Today `AbCipDataType.String` flattens any Logix `STRING` UDT into a .NET string via libplctag's `_tag.GetString(0)`. Logix programs commonly define `STRING_20`, `STRING_40`, `STRING_80` variants with different DATA-array sizes; libplctag honours these when the tag name resolves to the user-defined type, but our discovery emits them as the generic `String` placeholder. Add a `StringLength` field to `AbCipStructureMember` + `AbCipTagDefinition` so declared variants carry their cap, and thread it into the `Tag.Name` attribute or a libplctag string-cap hint. - **Files**: `AbCipDataType.cs`, `AbCipDriverOptions.cs` (record fields), `LibplctagTagRuntime.cs` (string-length aware decode/encode), and the discovery emit at `AbCipDriver.cs:715`. - **Test approach**: unit test with a fake runtime returning `string` values shorter and longer than the declared cap; integration test deferred until a sample L5X with mixed STRING variants is available. - **Effort**: M - **Dependencies**: investigate libplctag's `str_max_capacity` / `str_count_word_bytes` attributes — the docs reference them but the C# wrapper may not expose them; if not, this PR must extend `LibplctagTagRuntime` with a raw-buffer decode path. - **Docs / fixture / e2e**: extends `docs/Driver.AbCip.Cli.md` with a new `--string-size` flag in the `read`/`write` cookbook plus a STRINGnn worked example; updates `docs/drivers/AbServer-Test-Fixture.md` §"What it actually covers" to list `STRING_20`/`STRING_80` once seeded; extends the ControlLogix profile in `tests/.../Docker/docker-compose.yml` with `TestSTRING80:STRING[1]` (plus a `STRING_20` variant if `ab_server` honours non-default DATA caps; otherwise documented as Emulate-tier only); adds `tests/.../IntegrationTests/AbCipStringDecodingTests.cs` round-trip; adds a short-string round-trip case to `scripts/e2e/test-abcip.ps1` and a `TestSTRING80` row to `scripts/smoke/seed-abcip-smoke.sql`. #### PR 1.3 — Array-slice read addressing `Tag[0..N]` - **Scope**: today `AbCipTagPath` parses `Tag[3,5]` as a single element. Add slice syntax `Tag[0..15]` (parsed in `AbCipTagPath.TryParse`) and a planner that issues one libplctag read with `elem_count=N` per Rockwell array semantics, decoding the buffer at element stride into N output snapshots. Mirrors the whole-UDT planner pattern. - **Files**: `AbCipTagPath.cs` (parser — add `IsSlice` + `SliceLength` to the path segment record, or carry it on `AbCipTagPath` itself), new `AbCipArrayReadPlanner.cs` next to `AbCipUdtReadPlanner.cs`, `AbCipDriver.ReadAsync` to dispatch through the planner, `IAbCipTagRuntime` to add `DecodeArrayAt(type, elementStride, count)` or build on `DecodeValueAt`. Investigate libplctag's `elem_count` attribute on `Tag` create to confirm the right wire-level switch. - **Test approach**: parser unit tests for the new syntax, planner unit tests with fake runtime, integration smoke against a live ControlLogix DINT[100] tag using the dev-box PLC. - **Effort**: L - **Dependencies**: PR 1.1 must land first if the array element type is LINT — otherwise the slice path silently truncates 64-bit elements. - **Docs / fixture / e2e**: extends `docs/Driver.AbCip.Cli.md` `read` section with the `Tag[0..N]` slice syntax + a worked example reading `Recipe[0..15]` in one round-trip; updates `docs/drivers/AbServer-Test-Fixture.md` §"What it actually covers" to mention the existing `DINT[16]` array tag is now exercised end-to-end via slicing; extends `AbCipReadSmokeTests` with a slice-read assertion against the seeded `TestDINTArray`; adds `tests/.../IntegrationTests/AbCipArraySliceTests.cs` covering edge cases (boundary, single-element, full-range); adds a slice-read assertion to `scripts/e2e/test-abcip.ps1`. #### PR 1.4 — CIP multi-tag write packing - **Scope**: `AbCipDriver.WriteAsync` (`AbCipDriver.cs:460-546`) loops over writes one-by-one. Group writes by `(device, no-bit-RMW)` and submit one CIP Multi-Service Packet (0x0A) carrying up to N write-singles per round-trip. Honours the per-family `SupportsRequestPacking` flag at `AbCipPlcFamilyProfile.cs:36,43,51,59` — Micro800 falls back to the existing per-write loop because its profile already disables packing. - **Files**: `AbCipDriver.cs` (add a write planner mirroring the read planner), new `AbCipMultiWritePlanner.cs`, possibly a new `IAbCipTagRuntime.WriteBatchAsync` method or a new `IAbCipMultiWriter` capability since libplctag's high-level `Tag.WriteAsync` is per-tag — investigate libplctag's `cip-msg-multi` raw-CIP path or whether building a Multi-Service Packet via `plc_tag_create("name=@raw,...")` is feasible. - **Test approach**: unit test the planner with a synthetic batch (mixed-device, mixed bit-RMW, one Micro800); integration test recipe-style 50-tag write against ControlLogix measuring round-trip count via Wireshark or via a libplctag debug-trace sink. - **Effort**: L - **Dependencies**: investigate libplctag multi-service-packet API; if absent, this PR may need to drop down to raw CIP via the `@raw` pseudo-tag or be deferred. - **Docs / fixture / e2e**: appends a "Multi-tag writes" subsection to `docs/Driver.AbCip.Cli.md` (no flag — automatic batching when multiple writes queue inside one publish) plus a note that Micro800 falls back per profile; updates `docs/drivers/AbServer-Test-Fixture.md` §7 ("Capability surfaces beyond read") to flip `IWritable.WriteAsync` from "no smoke test" to covered for the multi-write path; adds `tests/.../IntegrationTests/AbCipMultiWriteTests.cs` asserting 50-tag batch lands in one round-trip (count via libplctag debug-trace sink); extends `scripts/e2e/test-abcip.ps1` with a recipe-style multi-write step; extends seed SQL with two extra DINT tags so the e2e has a packing target. ### Phase 2 — Tag-import workflows (4 PRs) Goal: replicate Kepware's Logix Database Settings — point the driver at an L5K/L5X export or a CSV and have the tag table populate without an online controller. #### PR 2.1 — L5K parser + ingest - **Scope**: parse a Studio 5000 L5K export (a labelled-section text format with `TAG ... END_TAG` blocks, `DATATYPE ... END_DATATYPE` UDT definitions, and program-scope qualifiers). Produce `AbCipTagDefinition` + `AbCipStructureMember` records that match the declarative options shape. Includes Description ingest (PR 2.3 lifts it to OPC UA `Description`). - **Files**: new `Import/L5kParser.cs`, new `Import/IL5kSource.cs` for testability, new `Import/L5kIngest.cs` that converts parsed records into `AbCipTagDefinition`. Hook into `AbCipDriverOptions` via a new `TagImports` collection (filenames or inline blobs) parsed on `AbCipDriver.InitializeAsync`. - **Test approach**: unit-only with sample L5K files in `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/Import/Fixtures/` covering controller-scope tags, program-scope tags, alias tags (skipped per Kepware precedent), and UDTs with nested structures. - **Effort**: L - **Dependencies**: none — pure-text parser. - **Docs / fixture / e2e**: new doc `docs/drivers/AbCip-TagImport.md` covering the L5K format support matrix (controller-scope / program-scope / UDT / alias-skipped) and a worked example of pointing `AbCipDriverOptions.TagImports` at an L5K export; appends a `tag-import` command section to `docs/Driver.AbCip.Cli.md` (CLI gains `tag-import --file foo.L5K`); fixture-side no change to `ab_server` (offline parse — no PLC needed) but adds sample L5K files under `tests/.../AbCip.Tests/Import/Fixtures/`; extends `scripts/e2e/test-abcip.ps1` with an offline `tag-import` smoke that diffs the parsed tag set against a golden JSON. #### PR 2.2 — L5X (XML) parser + ingest - **Scope**: same surface as PR 2.1 but parses Studio 5000's XML export. L5X is the de-facto modern format (Studio 5000 v21+) and carries richer metadata than L5K including ExternalAccess attributes and AOI definitions. - **Files**: new `Import/L5xParser.cs` using `System.Xml.XPath`, share the `IL5kSource` / `L5kIngest` ingest layer with PR 2.1 by introducing a common `ParsedTagsBundle` record. - **Test approach**: unit tests with sample L5X fixtures including an AOI-typed tag (sets up the AOI work in PR 2.7). - **Effort**: L - **Dependencies**: PR 2.1 is preferred first to settle the shared ingest seam. - **Docs / fixture / e2e**: extends `docs/drivers/AbCip-TagImport.md` (created in PR 2.1) with the L5X-specific section — namespace handling, ExternalAccess attributes, AOI references; extends the `tag-import` CLI section in `docs/Driver.AbCip.Cli.md` to note L5X auto-detection by file extension; sample L5X files added under `tests/.../AbCip.Tests/Import/Fixtures/` (one with an AOI-typed tag for PR 2.6); reuses the offline `tag-import` step from `scripts/e2e/test-abcip.ps1` (now driven by L5X) — no fixture container change because parse is offline; cross-links from `tests/.../IntegrationTests/LogixProject/README.md` so the on-site Emulate L5X export doubles as a parser fixture. #### PR 2.3 — Tag descriptions surfaced as OPC UA `Description` - **Scope**: extend `AbCipTagDefinition` with `Description` (string?), populate it from the L5K/L5X parsers, and thread it through to `DriverAttributeInfo` so the address-space builder sets the OPC UA `Description` attribute. Also lifts the description onto `AbCipStructureMember` for member-level metadata. - **Files**: `AbCipDriverOptions.cs` (record fields), `AbCipDriver.cs:760-770` (`ToAttributeInfo` helper), `Core.Abstractions/DriverAttributeInfo.cs` (verify it carries a Description field; if not, that becomes a Core PR shared across drivers). - **Test approach**: unit — a discovery test asserts that a tag with a description ends up with that description on the `DriverAttributeInfo` record. - **Effort**: S - **Dependencies**: PR 2.1 / PR 2.2 (descriptions only land via importer). - **Docs / fixture / e2e**: appends a "Description metadata" subsection to `docs/drivers/AbCip-TagImport.md` documenting how Studio 5000 descriptions surface as OPC UA `Description`; no CLI surface change (read-side only — the existing `otopcua-cli read` already projects `Description`); no fixture container change; adds a cross-driver assertion to the existing OPC UA browse test in `tests/.../IntegrationTests/` verifying the description survives the full parser → driver → server → client path; extends `scripts/e2e/test-abcip.ps1` with a one-line `Description != null` assertion after the import smoke step. #### PR 2.4 — CSV tag import / export - **Scope**: a CSV round-trip matching the Kepware column layout (`Tag Name, Address, Data Type, Respect Data Type, Client Access, Scan Rate, Description, Scaling`). Import populates `AbCipTagDefinition`; export dumps the live tag table for editing in Excel. - **Files**: new `Import/CsvTagImporter.cs`, new `Import/CsvTagExporter.cs`, integration point in `AbCipDriverOptions.TagImports` parallel to PR 2.1's hook. Export hook is exposed via the CLI (`docs/Driver.AbCip.Cli.md`) — add a `tag-export` command. - **Test approach**: unit tests for parser + writer with fixture CSVs; CLI integration test using a synthetic options payload. - **Effort**: M - **Dependencies**: lighter than 2.1/2.2 — could ship in either order, but landing CSV after L5X means the CSV export reuses the `ParsedTagsBundle` shape. - **Docs / fixture / e2e**: appends a "CSV tag table" section to `docs/drivers/AbCip-TagImport.md` documenting the column layout (Kepware-compatible) and round-trip semantics; appends `tag-export` and CSV-flavour `tag-import` commands to `docs/Driver.AbCip.Cli.md`; adds sample CSVs under `tests/.../AbCip.Tests/Import/Fixtures/` plus a CLI integration test (`tests/.../AbCip.Tests/Import/CsvRoundTripTests.cs`); extends `scripts/e2e/test-abcip.ps1` with an export-then-import-then-diff scenario (no PLC required); fixture-side no change. #### PR 2.5 — Online tag-DB refresh trigger (`$Sys$UpdateTagInfo` parity) - **Scope**: AVEVA exposes `$Sys$UpdateTagInfo` so an HMI can write `1` to force the driver to re-walk the controller's symbol table after a Studio 5000 download — without restarting the driver. Implement as a new `IDriverControl.RebrowseAsync()` invoked by the server or via a system-tag write (PR 4.4 will surface system tags as browseable variables — once that lands, this becomes the writeable system tag `_RefreshTagDb`). For now expose it via the CLI and via a new `AbCipDriver.RebrowseAsync` method. - **Files**: `AbCipDriver.cs` (new method that re-runs the `@tags` enumerator without going through full `ReinitializeAsync`), CLI command in `src/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/`, documentation update in `docs/Driver.AbCip.Cli.md`. - **Test approach**: unit test that two consecutive `RebrowseAsync` calls produce two enumeration passes; integration smoke against the dev-box ControlLogix verifying the address space picks up a tag added between rebrowses. - **Effort**: M - **Dependencies**: ties cleanly into PR 4.4 (system tags) but ships earlier as a programmatic API. - **Docs / fixture / e2e**: appends a `rebrowse` command to `docs/Driver.AbCip.Cli.md` with a Studio 5000 download recipe ("after a download, run `otopcua-abcip-cli rebrowse -g …`"); cross-references the future `_RefreshTagDb` system tag once PR 4.4 lands; updates `docs/drivers/AbServer-Test-Fixture.md` §7 to mark `ITagDiscovery.DiscoverAsync` as covered for the rebrowse path; adds `tests/.../IntegrationTests/AbCipRebrowseTests.cs` driving two consecutive enumerations (the second sees a tag added between calls — ab_server supports runtime reseed via its REST hook); extends `scripts/e2e/test-abcip.ps1` with a rebrowse-after-reseed assertion (or marks it `[OnlyIfRig]` if the simulator's reseed hook isn't reachable). #### PR 2.6 — AOI (Add-On Instruction) input/output handling - **Scope**: AOIs are first-class types in L5X (`AddOnInstructionDefinition` blocks). The Template Object decoder at `CipTemplateObjectDecoder.cs` likely already handles them at the wire level (an AOI is a Logix UDT with InOut/Input/Output qualifiers). This PR adds: (a) AOI-aware browse paths so an AOI instance shows up as a folder with `Inputs/`, `Outputs/`, `InOut/` sub-folders; (b) skip-on-discovery for `InOut` parameters per Kepware's documented limitation (InOut is a pointer, not a value). - **Files**: extend `AbCipStructureMember` with an `AoiQualifier` enum (Input/Output/InOut/Local), L5K/L5X parser extends to set it, `AbCipDriver.DiscoverAsync` groups members into qualifier-named sub-folders. - **Test approach**: unit test discovery against an AOI-containing fixture. - **Effort**: M - **Dependencies**: PR 2.2 (L5X) lands the AOI definition parsing. - **Docs / fixture / e2e**: appends an "AOI handling" section to `docs/drivers/AbCip-TagImport.md` covering Inputs/Outputs/InOut grouping + the InOut skip rationale; updates `docs/drivers/AbServer-Test-Fixture.md` §"What it does NOT cover" to keep AOIs flagged as ab_server-blocked but call out Logix Emulate as the authoritative tier; adds a sample AOI-bearing L5X under `tests/.../AbCip.Tests/Import/Fixtures/` and a discovery test that asserts the Inputs/Outputs sub-folder shape; promotes `tests/.../IntegrationTests/Emulate/AbCipEmulateAoiTests.cs` (gated on `AB_SERVER_PROFILE=emulate`) — no `scripts/e2e/test-abcip.ps1` change because AOIs need Emulate or a rig. ### Phase 3 — Performance levers (3 PRs) Goal: expose the protocol-mode + connection-tuning knobs that commercial drivers expose as device-level config. #### PR 3.1 — Configurable CIP Connection Size per device - **Scope**: today the family profile hard-codes 4002 / 504 / 488 at `AbCipPlcFamilyProfile.cs:33,42,49`. Add an optional `ConnectionSize` field to `AbCipDeviceOptions` that overrides the family default; thread it through to the libplctag tag-create attribute (`connection_size=N`). Validate against a sensible range (500-4002 per Kepware's slider). - **Files**: `AbCipDriverOptions.cs:70-73` (extend `AbCipDeviceOptions` record), `IAbCipTagRuntime.cs` (extend `AbCipTagCreateParams` with `ConnectionSize`), `LibplctagTagRuntime.cs` (set the `Tag.PlcType`-adjacent attribute — investigate libplctag C# wrapper's exposure of `connection_size`; may need to set via `Tag.AddAttribute` if a named property doesn't exist). - **Test approach**: unit test that custom Connection Size flows from options into the `AbCipTagCreateParams`; integration smoke against the dev-box ControlLogix verifying reduced-size connections succeed on legacy v19 firmware. Live test required because libplctag rejects out-of-range values silently in some versions. - **Effort**: S - **Dependencies**: investigate libplctag `connection_size` attribute exposure. - **Docs / fixture / e2e**: appends a "Connection Size" subsection to a new `docs/drivers/AbCip-Performance.md` (consolidates the Phase 3 knobs in one place) and a brief note + warning-symptom callout in `docs/Driver.AbCip.Cli.md` for the new per-device option in the Driver config; updates `docs/drivers/AbServer-Test-Fixture.md` §5 (CompactLogix narrow cap) noting that ab_server still doesn't enforce the cap so live coverage stays Emulate/rig-only; extends `scripts/smoke/seed-abcip-smoke.sql` with a `ConnectionSize` field demo; no `scripts/e2e/test-abcip.ps1` change (boot-time config knob, no per-call surface). #### PR 3.2 — Symbolic vs logical (instance-ID) addressing toggle - **Scope**: libplctag exposes `use_connected_msg=1&allow_packet_response_packing=1&logical_segment=1` (or similar — investigate the exact attribute name) for instance-ID addressing that skips per-poll ASCII parsing. Add a per-device `AddressingMode` enum (`Symbolic | Logical | Auto`) and thread it through `AbCipTagCreateParams`. `Auto` is the default and matches today's behaviour; `Logical` flips libplctag into instance-ID mode. Logical mode requires a one-time symbol-table walk to map names to instance IDs — reuse `LibplctagTagEnumerator` for the bootstrap. - **Files**: `AbCipDriverOptions.cs` (per-device enum), `IAbCipTagRuntime.cs` (`AbCipTagCreateParams.AddressingMode`), `LibplctagTagRuntime.cs` (translate to libplctag attributes), `AbCipDriver.cs` (run a one-time symbol-walk on first read in Logical mode). - **Test approach**: unit test attribute construction; integration benchmark — read 1000 tags in Symbolic vs Logical and assert >2x throughput on the dev-box ControlLogix. - **Effort**: L - **Dependencies**: investigate libplctag's instance-ID API; the mapping pseudo-tag is `@tags` (already used for browse) but the per-tag wire flag needs research. If libplctag doesn't expose this cleanly, the PR drops down to the raw `cip_addr` attribute. - **Docs / fixture / e2e**: appends an "Addressing mode" section to `docs/drivers/AbCip-Performance.md` (Symbolic / Logical / Auto trade-offs); adds a per-device `addressing-mode` knob to `docs/Driver.AbCip.Cli.md` (CLI gains `--addressing-mode` on `read`/`subscribe`/`write` for ad-hoc benchmarking); updates `docs/drivers/AbServer-Test-Fixture.md` §"What it actually covers" to add Logical-mode reads if ab_server's symbol table walks correctly under instance IDs (otherwise marked Emulate-tier-only); adds a benchmark test `tests/.../IntegrationTests/AbCipAddressingModeBenchTests.cs`; extends `scripts/e2e/test-abcip.ps1` with a Symbolic-vs-Logical sanity assertion (read 1000 tags both modes, assert Logical >= Symbolic throughput). #### PR 3.3 — Logical-blocking / non-blocking strategy selector - **Scope**: TOP Server names two modes: "logical-blocking" (whole-UDT read, decode members in-memory) and "logical-non-blocking" (per-member reads packed into one Multi-Service Packet). We have one direction shipped via `AbCipUdtReadPlanner`. Add a per-device `ReadStrategy` enum with three values: `WholeUdt` (current behaviour), `MultiPacket` (new: use libplctag request-packing to bundle per-member reads into one PDU when the UDT is sparse — i.e. only 2-of-50 members subscribed), and `Auto` (planner picks based on fraction-of-members-subscribed threshold). Strategy is per-device because Micro800 doesn't support packing. - **Files**: `AbCipDriverOptions.cs` (per-device enum), `AbCipUdtReadPlanner.cs` (add the threshold heuristic), new `AbCipMultiPacketReadPlanner.cs`, `AbCipDriver.ReadAsync` dispatch. Honours `AbCipPlcFamilyProfile.SupportsRequestPacking` at the family level so a user-selected `MultiPacket` on Micro800 falls back to per-tag with a warning logged. - **Test approach**: unit test the heuristic on synthetic batches of varying sparsity; integration benchmark with a 50-member UDT where 5 members are subscribed — verify MultiPacket beats WholeUdt by buffer-size delta. - **Effort**: L - **Dependencies**: PR 1.4 (multi-tag write packing) builds the same libplctag-multi-service primitive; landing 1.4 first reduces scope here. - **Docs / fixture / e2e**: appends a "Read strategy" section to `docs/drivers/AbCip-Performance.md` covering WholeUdt / MultiPacket / Auto plus the sparsity-threshold heuristic; updates `docs/drivers/AbServer-Test-Fixture.md` §1 (UDT coverage) with a note that strategy switching is decided in the planner and unit-tested only — Emulate is the authoritative wire-level coverage; adds `tests/.../IntegrationTests/Emulate/AbCipEmulateMultiPacketReadTests.cs` (gated on `AB_SERVER_PROFILE=emulate`); no CLI surface change beyond the existing per-device option, no `scripts/e2e/test-abcip.ps1` change because the simulator doesn't differentiate the two strategies on the wire. ### Phase 4 — Operability (4 PRs) Goal: make the driver behave like a SCADA driver — per-tag scan rates, write deadband, diagnostic system tags, online refresh trigger. #### PR 4.1 — Per-tag scan rate / scan group bucketing - **Scope**: today subscriptions key on a single `publishingInterval` per `_poll.Subscribe(...)` call. Add an optional `ScanRate` field to `AbCipTagDefinition` that, when set, overrides the subscription interval for that tag. The shared `PollGroupEngine` already buckets by interval — the change is to read the per-tag rate at subscribe-time and place the tag into its own bucket. - **Files**: `AbCipDriverOptions.cs` (record field), `AbCipDriver.SubscribeAsync` (look up per-tag override before passing to `_poll.Subscribe`). `PollGroupEngine` may need a new `Subscribe(tags, defaultInterval, perTagOverrides)` overload — check Core for the current signature. - **Test approach**: unit test that two tags with different ScanRate values produce two poll buckets; integration test verifying the faster-rate tag publishes more frequently than the slower-rate tag inside one subscription. - **Effort**: M - **Dependencies**: may require a small change to `PollGroupEngine` in Core. - **Docs / fixture / e2e**: new doc `docs/drivers/AbCip-Operability.md` (consolidates the Phase 4 knobs); appends a "Per-tag scan rate" section to it covering Kepware "scan classes" parity + the OPC UA publishing-interval interaction; no CLI surface change; fixture-side no change to ab_server; adds `tests/.../IntegrationTests/AbCipPerTagScanRateTests.cs` driving two tags at different rates against ab_server and asserting bucket separation; extends `scripts/e2e/test-abcip.ps1` with a two-tag subscribe-rate-divergence assertion. #### PR 4.2 — Write deadband / write-on-change - **Scope**: `AbCipDriver.WriteAsync` writes every request through. Add per-tag `WriteDeadband` (numeric) and `WriteOnChange` (boolean). When set, the driver tracks the last successfully-written value per `(tag, deviceHostAddress)` and suppresses the next write if `|new - last| < deadband` (numeric) or `new == last` (any). Suppressed writes return `Good` so OPC UA semantics are unaffected. - **Files**: `AbCipDriverOptions.cs` (record fields), new `AbCipWriteCoalescer.cs` holding the per-tag last-value cache, `AbCipDriver.WriteAsync` consults the coalescer before hitting the runtime. - **Test approach**: unit tests with synthetic writes — assert that a sequence of jittery setpoint values within deadband triggers a single PLC write. - **Effort**: M - **Dependencies**: none. - **Docs / fixture / e2e**: appends a "Write deadband / write-on-change" section to `docs/drivers/AbCip-Operability.md` with a worked setpoint-jitter example; updates `docs/drivers/AbServer-Test-Fixture.md` §7 to flip the multi-write coverage line to also cover suppression; adds `tests/.../IntegrationTests/AbCipWriteDeadbandTests.cs` driving a jittery setpoint and asserting the actual PLC write count via libplctag debug-trace; extends `scripts/e2e/test-abcip.ps1` with a write-coalesce assertion (write the same value twice, verify only one PLC-side change). #### PR 4.3 — Diagnostic / system tags as browseable variables - **Scope**: surface the `IHostConnectivityProbe` + `DriverHealth` data as browseable OPC UA variables under `AbCip//_System/`. Variables: `_ConnectionStatus`, `_ScanRate` (current effective publishing interval), `_TagCount`, `_DeviceError`, `_LastScanTimeMs`. Read-only; updated on each driver health transition. - **Files**: `AbCipDriver.DiscoverAsync` (`AbCipDriver.cs:674-758`) emits the system folder per device; new `AbCipSystemTagSource.cs` produces the live values; `ReadAsync` routes `_System/...` references to the source instead of the libplctag runtime. - **Test approach**: unit test that the discovery emits the expected nodes; unit test that reading a system tag returns the current health snapshot. - **Effort**: M - **Dependencies**: none, but PR 2.5 (online refresh trigger) becomes nicer once this lands — `_RefreshTagDb` writes `1` to invoke `RebrowseAsync`. - **Docs / fixture / e2e**: appends a "System tags / `_System` folder" section to `docs/drivers/AbCip-Operability.md` enumerating `_ConnectionStatus`, `_ScanRate`, `_TagCount`, `_DeviceError`, `_LastScanTimeMs`; cross-link from `docs/Driver.AbCip.Cli.md` (the `read` cookbook gains a system-tag example); updates `docs/drivers/AbServer-Test-Fixture.md` §7 to flip `IHostConnectivityProbe` state- transition coverage from "no" to covered (system tag observation provides the assertion hook); adds `tests/.../IntegrationTests/AbCipSystemTagDiscoveryTests.cs`; extends `scripts/e2e/test-abcip.ps1` with a `_System/_ConnectionStatus` browse-and-read step. #### PR 4.4 — Online tag-DB refresh trigger as `_RefreshTagDb` system tag - **Scope**: thin follow-up to PR 2.5 + PR 4.3 — wire the writeable system tag to the existing `RebrowseAsync` method. - **Files**: `AbCipSystemTagSource.cs` (writeable variable), `AbCipDriver.WriteAsync` intercepts `_RefreshTagDb` writes and dispatches to `RebrowseAsync`. - **Test approach**: unit + CLI integration. - **Effort**: S - **Dependencies**: PR 2.5 and PR 4.3. - **Docs / fixture / e2e**: extends the `_System` table in `docs/drivers/AbCip-Operability.md` to mark `_RefreshTagDb` as writeable; appends a "Refreshing the tag DB" recipe to `docs/Driver.AbCip.Cli.md` that pairs the system-tag write with the existing `rebrowse` command from PR 2.5; reuses the `AbCipRebrowseTests` fixture from PR 2.5 with an added system-tag-write entry point; extends `scripts/e2e/test-abcip.ps1` with a `_RefreshTagDb` write-then-verify assertion (chained off the rebrowse step from PR 2.5). ### Phase 5 — Redundancy (2 PRs) Goal: HSBY paired-IP failover for continuous-process plants. Heavier than the rest because it changes the `(device, hostName)` axiom — one logical device now has two host addresses. #### PR 5.1 — Paired host address syntax + role probing - **Scope**: extend `AbCipDeviceOptions` with `PartnerHostAddress` (optional). When set, the device probes both gateways concurrently using the existing probe loop machinery (`AbCipDriver.cs:235-281`). A ControlLogix HSBY pair exposes `WallClockTime`/`Module.Status` tags that identify the active chassis — investigate the exact tag name; `WallClockTime.SyncStatus` is one option, `S:34` (Module Status) carries the role bit on some versions. - **Files**: `AbCipDriverOptions.cs` (extend `AbCipDeviceOptions`), `AbCipDriver.cs` (extend `DeviceState` with `ActiveAddress` field, run two probe loops), new `AbCipHsbyRoleProber.cs` reading the role tag and returning Active/Standby. - **Test approach**: unit test with two fake probe runtimes returning different role bits; integration test deferred until a true HSBY pair is available — note in `MEMORY.md/project_aveva_platform_installed.md` that the dev box has a single chassis. - **Effort**: L - **Dependencies**: investigate the canonical HSBY role tag — the AVEVA ABCIP docs name it but the wire-level tag varies by firmware. - **Docs / fixture / e2e**: new doc `docs/drivers/AbCip-HSBY.md` covering the paired-IP config, the role-tag detection matrix (v20 / v24 / v32+), and the feature-flag gate (`Redundancy.Hsby.Enabled`); extends `docs/Driver.AbCip.Cli.md` with a `--partner` flag plus an `hsby-status` command that prints the active partner; updates `docs/drivers/AbServer-Test-Fixture.md` §"What it does NOT cover" with a new entry marking HSBY as ab_server-blocked but adds a "paired-fixture" mode to `tests/.../Docker/docker-compose.yml` (two `controllogix` services on different ports + a `hsby-mux` sidecar that flips the role bit on demand); adds `tests/.../IntegrationTests/AbCipHsbyRoleProberTests.cs`; no `scripts/e2e/test-abcip.ps1` change yet — HSBY e2e is gated behind a sibling `scripts/e2e/test-abcip-hsby.ps1` script introduced in PR 5.2. #### PR 5.2 — Failover routing in IPerCallHostResolver - **Scope**: `AbCipDriver.ResolveHost` returns the device's primary address today (`AbCipDriver.cs:307-312`). Change it to return the currently-Active partner. On role transition, the existing bulkhead/breaker per-host keying isolates a stuck primary without affecting the failover path because the partner address has its own breaker. - **Files**: `AbCipDriver.cs:ResolveHost` consults `DeviceState.ActiveAddress`, plus a small change to per-tag runtime caching so handles are keyed on the active address — failover invalidates the handle cache and re-creates against the new gateway. - **Test approach**: unit test that toggling the role flag flips `ResolveHost` output; integration test deferred per PR 5.1. - **Effort**: M - **Dependencies**: PR 5.1. - **Docs / fixture / e2e**: appends a "Failover behaviour" section to `docs/drivers/AbCip-HSBY.md` documenting handle-cache invalidation + bulkhead key semantics; appends a "Failure-mode walkthrough" to the same doc covering primary-stuck / secondary-stuck / both-stuck cases; reuses the paired-fixture from PR 5.1; adds `tests/.../IntegrationTests/AbCipHsbyFailoverTests.cs` driving the role-flip via the `hsby-mux` sidecar and asserting reads route to the new active partner; ships the new `scripts/e2e/test-abcip-hsby.ps1` (paired-fixture variant of the standard e2e — flips the role mid-stream and asserts subscribe stream survives). ## Per-PR detail The summary above already includes each PR's title, motivation (linked to the featuregaps.md table row), files, test plan, and effort. To keep this section from duplicating, here are the cross-cutting design notes and risks per phase rather than per PR. ### Phase 1 risks - **Int64 surface change** (PR 1.1) ripples through the address-space builder + the OPC UA variant emit. Confirm `Core.Abstractions.DriverDataType` already has `Int64`; if not, this PR pulls in a Core change other drivers will share (Modbus has the same TODO). - **STRINGnn variant addressing** (PR 1.2) is the smallest data-correctness PR but has the highest unknown — libplctag's C# wrapper may flatten all string variants to its built-in `GetString(0)` helper. If true, PR 1.2 must add a raw-buffer decode path and is then upgraded from M to L. - **Array-slice planner** (PR 1.3) introduces a third planner alongside the UDT planner + the future write planner (1.4). Build them on a shared `IAbCipReadPlanner` seam so Phase 3's strategy selector has one slot to pivot on, not three. - **Multi-write packing** (PR 1.4) hinges on libplctag exposing CIP Multi-Service Packet construction. If it does not, the work-around is a raw-CIP `@raw` send, which is a bigger lift and may push 1.4 to an L-plus that drags into Phase 3. ### Phase 2 risks - **L5K text format** has documented edge cases (escape sequences in DESCRIPTION strings, alias resolution, nested DATATYPE blocks). Lean on Rockwell's published L5K BNF and treat unknown sections as warnings, not failures. - **L5X namespace handling** — Studio 5000 v32+ adds optional XML namespaces. Use XPath with prefix-agnostic queries to avoid version-pinning the parser. - **CSV column drift** — Kepware's column order has shifted over major versions. Implement the importer to read by header name, not column index. ### Phase 3 risks - **Logical addressing bootstrap cost** (PR 3.2) — symbol-table walk on first read can stall the first poll batch. Cache the instance-ID map per `(device, last symbol-table hash)` and persist it across `ReinitializeAsync` if feasible. - **MultiPacket vs WholeUdt heuristic** (PR 3.3) — the threshold (e.g. "switch to MultiPacket when fewer than 30% of UDT members are subscribed") needs benchmarking on real rigs. Ship an explicit per-device override + pick a conservative default. - **Connection Size on legacy firmware** (PR 3.1) — v19-and-earlier ControlLogix firmware rejects Large Forward Open silently. Document the symptom in `docs/Driver.AbCip.md` and emit a warning when ConnectionSize > 511 against a family profile that is ControlLogix-typed but probed-as-v19. ### Phase 4 risks - **Per-tag scan rate** (PR 4.1) interacts with the OPC UA subscription's publishing-interval contract. Document that the per-tag override is a *driver-side* publish bucket that fires `OnDataChange` events at the per-tag rate; the OPC UA layer still aggregates them on its own publishing-interval and the client may see them at the larger of the two intervals. This matches Kepware's "scan classes" semantics. - **Write deadband** (PR 4.2) on UDT-fanned-out members must use the member-level cache, not the parent UDT's cache. ### Phase 5 risks - **HSBY role tag name** (PR 5.1) varies by firmware version; without a real HSBY pair on the dev box the integration coverage is deferred to a customer-site smoke test. Consider parking PR 5.1+5.2 behind a feature flag (`Redundancy.Hsby.Enabled`) and shipping unit coverage only. - **Bulkhead key** assumed to be `(driver, hostName)`; once `ResolveHost` returns the active partner address that key is correct by construction, but verify Polly's per-key state is invalidated cleanly when the active address changes mid-call. ## Documentation, fixture, and e2e impact Cross-cutting roll-up of the per-PR `Docs / fixture / e2e` lines above. Read this before starting any phase to plan doc + fixture + e2e work in parallel with the code change. ### New documents - `docs/drivers/AbCip-TagImport.md` (Phase 2, lands with PR 2.1; extended by PR 2.2, PR 2.3, PR 2.4, PR 2.6) — L5K / L5X / CSV / AOI tag-import reference. - `docs/drivers/AbCip-Performance.md` (Phase 3, lands with PR 3.1; extended by PR 3.2, PR 3.3) — Connection Size, Addressing Mode, Read Strategy. - `docs/drivers/AbCip-Operability.md` (Phase 4, lands with PR 4.1; extended by PR 4.2, PR 4.3, PR 4.4) — per-tag scan rate, write deadband, system tags. - `docs/drivers/AbCip-HSBY.md` (Phase 5, lands with PR 5.1; extended by PR 5.2) — paired-IP redundancy, role-tag matrix, failover semantics. ### Documents with appended sections - `docs/Driver.AbCip.Cli.md` — gains type-table rows (PR 1.1), `--string-size` flag (PR 1.2), slice syntax (PR 1.3), multi-write subsection (PR 1.4), `tag-import` / `tag-export` commands (PR 2.1, PR 2.2, PR 2.4), `rebrowse` command (PR 2.5), Connection Size note (PR 3.1), `--addressing-mode` flag (PR 3.2), system-tag read example (PR 4.3), `_RefreshTagDb` recipe (PR 4.4), `--partner` flag plus `hsby-status` command (PR 5.1). - `docs/drivers/AbServer-Test-Fixture.md` — coverage map updated by every PR that changes what ab_server actually exercises (1.1, 1.2, 1.3, 1.4, 2.6, 3.1, 3.2, 3.3, 4.2, 4.3, 5.1). ### Fixture / simulator scaffolding - `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/docker-compose.yml` — ControlLogix profile gains seeded `TestLINT`, `TestSTRING80`, extra DINT tags for multi-write (PRs 1.1, 1.2, 1.4); a new paired-fixture mode with `hsby-mux` sidecar for HSBY (PR 5.1). - `tests/.../AbCip.IntegrationTests/AbServerProfile.cs` — the `KnownProfiles` records get extended Notes lines for each new seeded tag class. - `tests/.../AbCip.Tests/Import/Fixtures/` — new directory hosting sample L5K, L5X, and CSV files (PRs 2.1, 2.2, 2.6, 2.4). - `tests/.../AbCip.IntegrationTests/Emulate/` — new gated tests for AOI (PR 2.6) and MultiPacket strategy (PR 3.3); reuses the existing `AB_SERVER_PROFILE=emulate` gate. - `tests/.../AbCip.IntegrationTests/LogixProject/README.md` — cross-link added when PR 2.2 lands so the on-site Studio 5000 export doubles as a parser fixture. ### Integration / e2e scripts - `scripts/e2e/test-abcip.ps1` — gains assertions for: LInt loopback (1.1), STRING round-trip (1.2), array-slice read (1.3), recipe multi-write (1.4), tag-import diff (2.1, 2.2, 2.4), Description survival (2.3), rebrowse-after-reseed (2.5), Symbolic-vs-Logical sanity (3.2), per-tag scan-rate divergence (4.1), write-coalesce (4.2), `_System` browse-and-read (4.3), `_RefreshTagDb` write-then- verify (4.4). - `scripts/smoke/seed-abcip-smoke.sql` — extended with `TestLINT`, `TestSTRING80`, multi-write target tags, and a `ConnectionSize` field demo (PRs 1.1, 1.2, 1.4, 3.1). - `scripts/e2e/test-abcip-hsby.ps1` — new paired-fixture variant of the standard e2e, ships with PR 5.2; not chained into `scripts/e2e/test-all.ps1` until HSBY exits feature-flag gating. ### Cross-cutting work - The `Docs / fixture / e2e` lines deliberately reuse the existing `Test-Probe` / `Test-DriverLoopback` / `Test-ServerBridge` / `Test-OpcUaWriteBridge` / `Test-SubscribeSeesChange` helpers in `scripts/e2e/_common.ps1` — no new helper functions are required for Phases 1-4. Phase 5 is the first phase that introduces a new helper (`Test-FailoverDuringSubscribe`) in `_common.ps1`, shipped alongside PR 5.2; if other drivers (TwinCAT, S7) later adopt a paired-fixture mode they can reuse it. - `tests/.../AbCip.IntegrationTests/AbServerFixture.cs` may need a small extension in PR 5.1 to support the paired-port probe; the change is additive (probe both `127.0.0.1:44818` and `127.0.0.1:44819`), keeping single-fixture tests working unchanged. ## Skip-rated items (for context) Copied from the Recommendations table at `docs/featuregaps.md`: - **#7 Inactivity timeout / keep-alive cadence** — Rarely an issue with libplctag-managed connections. - **#9 "Respect tag-specified scan rate" mode** — Niche; OPC UA subscription rate already covers it. - **#10 Initial value cache / first-update from cache** — OPC UA subscription sampling already handles first-update. - **#15 UDT as first-class OPC UA structured type** — Member fan-out already works; structured-type plumbing is heavy. - **#17 PLC-5 / SLC bridging through CLX** — AbLegacy driver covers this protocol family. - **#21 Unsolicited CIP MSG ingestion** — Separate driver in commercial; design-heavy; niche. - **#22 CIP Generic / Class 3 passthrough** — Niche custom-tooling territory. - **#23 Per-device connection count / pooling** — libplctag manages connections; premature. ## Open questions 1. **libplctag instance-ID API** (PR 3.2) — does the C# wrapper expose `logical_segment` / `cip_addr` attributes directly, or do we have to drop down to `Tag.AddAttribute` calls? Affects scope of Phase 3. 2. **libplctag CIP Multi-Service Packet** (PR 1.4) — is there a wrapper-level multi-write helper, or must we go through the `@raw` pseudo-tag? Affects scope of Phase 1. 3. **`DriverDataType.Int64` / `Int64Array`** (PR 1.1) — does Core already carry it, or is this a shared Core change with Modbus's matching TODO? 4. **HSBY role tag** (PR 5.1) — confirm the canonical Active/Standby indicator across ControlLogix v20 / v24 / v32+; without a known tag the role-prober is speculative. 5. **AOI InOut handling** (PR 2.6) — Kepware skips InOut parameters because they are pointers, not values. Do we follow the same precedent or attempt to dereference at read-time? Skip is the cheap default. 6. **L5K vs L5X coverage** — if the customer base has standardised on L5X (Studio 5000 v21+), can we ship PR 2.2 first and make PR 2.1 best-effort? Affects phasing within Phase 2. 7. **HSBY scope for v2 vs v3** — Phase 5 carries the largest unknowns; if no continuous- process customer demands it for the v2 release, deferring Phase 5 to v3 is reasonable. 8. **Per-tag scan rate plumbing** (PR 4.1) — does `PollGroupEngine` in Core already accept per-reference interval overrides, or does that need a Core extension shared with the other polling-overlay drivers (Modbus, FOCAS)?