# AbLegacy Driver — Implementation Plan > Source of gap analysis: [featuregaps.md → AbLegacy](../featuregaps.md#ablegacy-allen-bradley-plc-5--slc--micrologix) > > Covers Build = Yes items only. Skip-rated gaps listed at bottom for traceability. ## Summary The AbLegacy driver (PCCC over EtherNet/IP via libplctag) currently ships with parsing for the canonical SLC/PLC-5/MicroLogix file letters, four PLC-family profiles, bit-within-N-word RMW writes, a probe loop, and a flat static-config tag list. The `featuregaps.md` Recommendations table flags 13 gaps as **Build = Yes**: 1. DH+ via 1756-DHRIO bridging (#2) 2. PD/MG/PLS/BT files (#5) 3. PLC-5 octal addressing (#7) 4. Indirect/indexed addressing (#8) 5. Array contiguous block addressing (#9) 6. ST string read/write production verification (#10) 7. Sub-element bit semantics (`.DN` as Bit) (#11) 8. Auto-demote on comm failure (#13) 9. RSLogix 500/5 symbol import (#15) 10. Per-tag deadband / change filter (#18) 11. Diagnostic counters as tags (#20) 12. Per-device timeout / retry overrides (#21) 13. MicroLogix function-file naming (RTC/HSC/DLS) (#23) The plan splits these across **5 phases / 13 PRs** (one PR per gap, with a couple of small ones bundled). Phases are ordered by coupling — addressing correctness first because everything downstream depends on the parser, then file/type coverage, then performance, then workflow tooling, then resilience. Each PR is sized to fit comfortably under the project's per-PR review budget (most S/M; only the RSLogix import is L). ## Phased delivery | Phase | Theme | PRs | Gaps | |-------|-------|-----|------| | 1 | Addressing correctness | 4 | #7 octal, #8 indirect, #11 sub-element bits, #23 ML function files | | 2 | File / type coverage | 2 | #5 PD/MG/PLS/BT, #10 ST verification | | 3 | Performance | 2 | #9 array block, #18 per-tag deadband | | 4 | Workflow | 3 | #15 RSLogix import, #21 per-device timeouts, #20 diagnostic counters | | 5 | Resilience | 2 | #13 auto-demote, #2 DH+ bridging | Phase 1 lands first because Phase 2 (PD/MG/PLS/BT) and Phase 3 (array reads) both extend the parser shipped in Phase 1. Phase 5 (auto-demote) reads diagnostic counters from Phase 4 #20, so 4 precedes 5. --- ## Per-PR detail ### Phase 1 — Addressing correctness #### PR 1 — PLC-5 octal I/O addressing (#7) **Scope**: PLC-5 documentation and RSLogix 5 use octal for `I:` / `O:` word and bit indices (`I:001/17` is rack 0 group 0 word 1, bit 17₈ = bit 15₁₀). Today `AbLegacyAddress.TryParse` does `int.TryParse` on the word number and bit index, silently accepting decimal. For `PlcFamily=Plc5` (and only that family) `I` / `O` files must parse as octal. **Files**: - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyAddress.cs` — add `TryParse(string, AbLegacyPlcFamily)` overload; existing `TryParse(string)` keeps decimal semantics (back-compat for non-PLC-5 callers and pure shape validation). - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs` — `EnsureTagRuntimeAsync` and the bit-RMW path call the family-aware overload using `device.Options.PlcFamily`. - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/PlcFamilies/AbLegacyPlcFamilyProfile.cs` — add `OctalIoAddressing` flag (true for `Plc5` only). **Test plan**: - Unit (`tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyAddressTests.cs`): `I:001/17` parses to word=1, bit=15 under PLC-5; same string parses to bit=17 under SLC500. `O:7/10` (decimal under SLC500 = bit 10; octal under PLC-5 = bit 8). - Round-trip: `ToLibplctagName()` must emit the format libplctag expects (verify libplctag's PLC-5 PCCC layer accepts octal-formatted I/O addresses, or whether we must convert decimal→octal-text before forwarding). **Docs / fixture / e2e**: - Update `docs/Driver.AbLegacy.Cli.md` — extend the "PCCC address primer" with an `I:` / `O:` row noting PLC-5 octal vs SLC500 decimal semantics; worked example showing `I:001/17` resolved differently per family. - Update `docs/drivers/AbLegacy-Test-Fixture.md` — note octal-vs-decimal addressing as a covered family-aware parser dimension under the unit-coverage list. - Fixture: extend `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/docker-compose.yml` `plc5` profile to seed an `I:001` (or equivalent module-image word) tag if `ab_server --plc=PLC/5` accepts it; otherwise document the gap in `Docker/README.md`. - E2E: add `--plc-type Plc5 -a "I:001/17"` octal-bit assertion to `scripts/e2e/test-ablegacy.ps1` (gated on the `plc5` compose profile being up); no change to `scripts/smoke/seed-ablegacy-smoke.sql` required (existing `N7:5` tag continues to cover the SLC500 path). **Effort**: S **Dependencies**: none --- #### PR 2 — MicroLogix function-file letters (RTC / HSC / DLS / MMI / PTO / PWM / STI / EII / IOS / BHI) (#23) **Scope**: MicroLogix 1100/1400 expose proprietary function files that don't share file letters with SLC. Today `IsKnownFileLetter` (`AbLegacyAddress.cs:97-101`) only allows the SLC/PLC-5 set, so any tag like `RTC:0.HR` is rejected at parse time even though libplctag's `micrologix` PlcType supports them. **Files**: - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyAddress.cs` — extend `IsKnownFileLetter` to recognise multi-letter function-file types (`RTC`, `HSC`, `DLS`, `MMI`, `PTO`, `PWM`, `STI`, `EII`, `IOS`, `BHI`). Permit only when family is `MicroLogix`. The letter-scan loop already accepts any contiguous letters (`AbLegacyAddress.cs:80-82`). - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDataType.cs` — define a sub-element catalogue per function-file (RTC has YR/MON/DAY/HR/MIN/SEC/DOW; HSC has ACC/HIP/LOP/OFS/etc.). Map each sub-element to the right `DriverDataType`. - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/PlcFamilies/AbLegacyPlcFamilyProfile.cs` — `SupportsFunctionFiles` flag. **Test plan**: - Unit: `RTC:0.HR` parses with `FileLetter="RTC"`, `WordNumber=0`, `SubElement="HR"`. `HSC:0.ACC` parses. Same strings under PlcFamily=Slc500 must reject (ML1100 file types not present on SLC). - Integration (`tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests`): only if a MicroLogix simulator profile exists; flag as TODO otherwise — verify libplctag `micrologix` PlcType accepts these tag names. **Docs / fixture / e2e**: - New doc `docs/drivers/AbLegacy-MicroLogix-FunctionFiles.md` — catalogue of supported function files (RTC/HSC/DLS/MMI/PTO/PWM/STI/EII/IOS/BHI), per-family availability matrix (ML1100 vs ML1400 vs ML1500), sub-element-to-DriverDataType table. - Update `docs/Driver.AbLegacy.Cli.md` — add a "MicroLogix function files" row to the PCCC address primer with `RTC:0.HR` / `HSC:0.ACC` examples and a CLI worked example. - Update `docs/drivers/AbLegacy-Test-Fixture.md` — record fixture coverage status for function files and link to the `micrologix` profile gap (only if `ab_server --plc=Micrologix` rejects function-file addresses, document the unit-only fallback). - Fixture: extend `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/docker-compose.yml` `micrologix` profile with `--tag=RTC0[1]` / `--tag=HSC0[1]` if accepted by `ab_server`, else mark as hardware-gated in `Docker/README.md`. - E2E: add a parametric `-PlcType MicroLogix -Address RTC:0.HR` invocation to `scripts/e2e/test-ablegacy.ps1` (skip-when-fixture-gap, mirroring the existing `BadCommunicationError` gate); no `seed-ablegacy-smoke.sql` change unless the fixture supports function-file tags. **Effort**: M **Dependencies**: PR 1 (parser overload signature settled) --- #### PR 3 — Sub-element bit semantics (`.DN`, `.EN`, `.TT`, `.CU`, `.CD`, `.OV`, `.UN`, `.ER`) (#11) **Scope**: Today `T4:0.DN` parses fine but the `TimerElement`/`CounterElement`/`ControlElement` types collapse to `Int32` (`AbLegacyDataType.cs:41-44`). HMIs expect `.DN` / `.EN` / `.TT` / `.CU` / `.CD` / `.OV` / `.UN` / `.ER` to surface as `Boolean`. The fix is to detect the sub-element at tag-runtime build time and override the driver-surface type. **Files**: - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDataType.cs` — new helper `SubElementBitNames` (HashSet of bit-typed sub-elements per parent type — Timer: EN/TT/DN; Counter: CU/CD/DN/OV/UN; Control: EN/EU/DN/EM/ER/UL/IN/FD). New `EffectiveDriverDataType(AbLegacyDataType, string? subElement)` returning `Boolean` for bit-typed sub-elements, otherwise the existing mapping. - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs` — `DiscoverAsync` uses `EffectiveDriverDataType(def.DataType, parsed.SubElement)`; `ReadAsync` decodes the parent word and masks the bit instead of returning the whole word as Int32. - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs` — verify libplctag exposes `.DN` etc. as a single bit when read with `GetBit` against the sub-element address. If not, fall back to read-the-word + mask. **Test plan**: - Unit (`AbLegacyDriverTests` + new `AbLegacyDataTypeTests`): `T4:0.DN` discovers as Boolean; `T4:0.ACC` discovers as Int32; counter `.OV` is Boolean; control `.LEN` is Int32. - Bit-write semantics: writing Boolean `true` to `T4:0.DN` should be rejected with `BadNotWritable` (timer status bits are PLC-set; verify by integration smoke test against the AbLegacy simulator). **Docs / fixture / e2e**: - Update `docs/Driver.AbLegacy.Cli.md` — extend the Timer/Counter/Control rows in the address primer with a "bit sub-elements surface as Boolean" note and a `--type Bool -a T4:0.DN` CLI example. - Update `docs/drivers/AbLegacy-Test-Fixture.md` — note `AbLegacyDataTypeTests` as a new unit-coverage class under "What it actually covers". - Fixture: no compose change required (T4/C5/R6 already seeded by `ab_server` defaults — verify; if not, add `--tag=T4[5]`/`--tag=C5[5]`/`--tag=R6[5]` to the `slc500` profile in `Docker/docker-compose.yml`). - E2E: extend `scripts/e2e/test-ablegacy.ps1` with a Boolean sub-element read assertion (`read --type Bool -a T4:0.DN`) once the simulator round-trip works. Update `scripts/smoke/seed-ablegacy-smoke.sql` to add a Boolean tag binding `T4:0.DN` so the server-bridge assertion exercises the new mapping. **Effort**: M **Dependencies**: none (independent of PR 1/2 parser changes) --- #### PR 4 — Indirect / indexed addressing parser (`N7:[N7:0]`, `N[N7:0]:5`) (#8) **Scope**: Recipe / batch lookup tables use `N7:[N7:0]` (read N7 word indexed by the value at N7:0) or `N[N7:0]:5`. Today `AbLegacyAddress.TryParse` rejects both because it requires literal integer word and file numbers. **Files**: - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyAddress.cs` — record gains nullable `IndirectFileSource` and `IndirectWordSource` (each itself an `AbLegacyAddress`). Parser handles `[]` segments at file-number or word-number positions. Recursion depth capped at 1 (libplctag accepts only one level of indirection per address — verify against libplctag PCCC docs). - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDataType.cs` — no change. - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs` — pass-through; `ToLibplctagName()` re-emits the bracket form. **Test plan**: - Unit: `N7:[N7:0]` → outer file=N7, indirect word source = (N, 7, 0); `B3:[N7:0]/0` → bit, indirect word source = (N, 7, 0); `N[N7:0]:5` → indirect file source = (N, 7, 0), word=5; depth-2 (`N[N[N7:0]:5]:0`) must reject. - Integration: verify libplctag's `slc500`/`plc5` PlcType accepts a `Name` of form `N7:[N7:0]` and resolves at read time. (If libplctag rejects indirect text, fall back to two-step read: resolve the inner address, then read the outer with the resolved index. Document the chosen strategy in the PR.) **Docs / fixture / e2e**: - New doc `docs/drivers/AbLegacy-Indirect-Addressing.md` — explain `N7:[N7:0]` and `N[N7:0]:5` syntax, the depth-1 limit, the chosen libplctag strategy (verbatim pass-through vs two-step resolve), and recipe-table use cases. - Update `docs/Driver.AbLegacy.Cli.md` — add an indirect-addressing row to the address primer with `--address "N7:[N7:0]"` example. - Update `docs/drivers/AbLegacy-Test-Fixture.md` — under unit coverage, list `AbLegacyAddressTests` indirect-parsing cases. - Fixture: no `Docker/docker-compose.yml` change required (`N7[10]` already seeded; the inner index tag at `N7:0` is already addressable). Document recipe-pattern in `Docker/README.md`. - E2E: extend `scripts/e2e/test-ablegacy.ps1` with an indirect-address driver-loopback case (write to `N7:0` to set the index, then read `N7:[N7:0]` and assert the value matches the previously-written content of the resolved word). Skip-gate behind libplctag capability check. **Effort**: M **Dependencies**: PR 1 (octal resolution must apply to inner address too if the outer file is `I:`/`O:` on PLC-5) --- ### Phase 2 — File / type coverage #### PR 5 — PD / MG / PLS / BT structure files (#5) **Scope**: Add PD (PID), MG (Message), PLS (Programmable Limit Switch), BT (Block Transfer) file types to the parser and the data-type catalogue. PD has SP/PV/CV/Error/Bias plus 25+ sub-elements; MG has Error/Length/Position/etc.; PLS has LEN/POS; BT is similar to MG. **Files**: - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyAddress.cs` — extend `IsKnownFileLetter` with `PD`, `MG`, `PLS`, `BT`. - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDataType.cs` — new enum members `PidElement`, `MessageElement`, `PlsElement`, `BlockTransferElement`. Sub-element catalogue per type — many PD sub-elements are Float32 (`SP`, `PV`, `CV`, `KP`, `KI`, `KD`), some are Boolean (`EN`, `DN`, `MO`, `PE`), some Int16 (`SPS`, `MAXS`, `MINS`). - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs` — verify libplctag PCCC supports addressing PD/MG/PLS/BT sub-elements by name; if not, the driver reads the parent struct as a byte block and offsets internally (libplctag docs to consult). - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/PlcFamilies/AbLegacyPlcFamilyProfile.cs` — `SupportsPidFile` etc. flags (PLC-5 supports PD/BT; SLC supports PD; ML1100/1400 generally do not — verify per family docs). **Test plan**: - Unit: `PD9:0.SP` → Float32; `PD9:0.EN` → Boolean; `MG10:0.LEN` → Int32; reject `PD9:0` (no sub-element on a struct file). - Integration: smoke test against a simulator with PD file configured (verify pylogix/pycomm3 sim supports PD, otherwise mark as TODO and lean on unit coverage). **Docs / fixture / e2e**: - New doc `docs/drivers/AbLegacy-Structure-Files.md` — sub-element catalogues for PD / MG / PLS / BT, per-family availability matrix (PLC-5 vs SLC vs ML), DriverDataType per sub-element. - Update `docs/Driver.AbLegacy.Cli.md` — add PD / MG / PLS / BT rows to the file-letter primer with `--type PidElement` etc. examples. - Update `docs/drivers/AbLegacy-Test-Fixture.md` — list new structure-file file letters under unit coverage and note any fixture limitations (pd/mg likely not supported by `ab_server`). - Fixture: extend `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/docker-compose.yml` `slc500` and `plc5` profiles with `--tag=PD9[2]` / `--tag=MG10[2]` if `ab_server` accepts; otherwise document gap in `Docker/README.md` and rely on unit coverage. - E2E: extend `scripts/e2e/test-ablegacy.ps1` with a `read --type Float -a PD9:0.SP` assertion when fixture exposes the file; add a corresponding tag row to `scripts/smoke/seed-ablegacy-smoke.sql` (skip-gated). **Effort**: M **Dependencies**: PR 3 (sub-element bit semantics machinery must exist first — PD `.EN` is Boolean by the same mechanism as Timer `.EN`) --- #### PR 6 — ST string read/write production verification (#10) **Scope**: ST is enum-listed and `LibplctagLegacyTagRuntime.DecodeValue` calls `_tag.GetString(0)`, but there's no integration coverage that ST round-trips through libplctag's 82-byte length-word format. This PR is verification + any fixes uncovered. **Files**: - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs` — likely no source change if libplctag's `GetString`/`SetString` already handles the length-word convention; if not, add `GetByteArrayBuffer` + manual length-word decode. - `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyReadSmokeTests.cs` — add `ST_RoundTrip_*` tests against the simulator: write 82-char string, write 0-char, write 41-char, write embedded null/non-ASCII; round-trip each through ReadAsync. - New `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyStringEncodingTests.cs` — unit-level decode of a known length-word + payload byte buffer (mock `IAbLegacyTagRuntime` returning fixed bytes). **Test plan**: - Integration: 4 round-trip cases above; covers PlcFamily=Slc500 and PlcFamily=Plc5 (libplctag may handle the length word differently between the two PCCC layers — verify). - Quality: unit test that `BadOutOfRange` surfaces when caller writes a 100-char string to an 82-byte ST. **Docs / fixture / e2e**: - Update `docs/Driver.AbLegacy.Cli.md` — expand the `ST` row in the address primer with the 82-byte limit, length-word convention, and a `write --type String --value "Hello"` worked example. - Update `docs/drivers/AbLegacy-Test-Fixture.md` — list the new `AbLegacyStringEncodingTests` unit class and the four `ST_RoundTrip_*` integration cases under coverage. - Fixture: extend `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/docker-compose.yml` `slc500` and `plc5` profiles with `--tag=ST20[5]` so the round-trip tests have a real address to write against; document any `ab_server` ST gaps in `Docker/README.md`. - E2E: extend `scripts/e2e/test-ablegacy.ps1` with a String round-trip case (`-a "ST20:0" --type String`) and a `String` tag row in `scripts/smoke/seed-ablegacy-smoke.sql` so the bridge assertion exercises ST. **Effort**: S (mostly tests; small encoding fix if any) **Dependencies**: none --- ### Phase 3 — Performance #### PR 7 — Array contiguous block addressing (`N7:0,10` or `N7:0[10]`) (#9) **Scope**: One PCCC frame can pull up to ~120 words. Today every tag is a separate libplctag instance and a separate request. The fix exposes array tags as a single tag with `IsArray=true` + `ArrayDim`, backed by a libplctag tag with `elem_count=N`. **Files**: - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyAddress.cs` — record gains `ArrayCount` (nullable). Parser accepts `,N` suffix (Rockwell convention) and `[N]` suffix (libplctag convention) on the word number. Reject combination with sub-element or bit index. - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverOptions.cs` — `AbLegacyTagDefinition` gains optional `ArrayLength` (overrides parsed value; convenient when address is parameterised). - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/IAbLegacyTagRuntime.cs` — `AbLegacyTagCreateParams` gains `ElementCount` (default 1). - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs` — pass `ElementCount` to libplctag `Tag.ElementCount` (verify libplctag supports element counts on PCCC PlcTypes — it does for ab_eip CIP tags but PCCC may behave differently). - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs` — `DiscoverAsync` emits `IsArray=true`, `ArrayDim=[N]`; `ReadAsync` decodes via per-index `_tag.GetInt16(i*2)` etc. **Test plan**: - Unit: `N7:0,10` parses ArrayCount=10; `N7:0[10]` same; `N7:0,10/3` rejects (array+bit); `T4:0,5.ACC` rejects (array+sub-element). - Integration: read `N7:0,10` returns 10 elements in one frame; latency measurement vs 10 individual tags should be ≥ 5x faster (target). **Docs / fixture / e2e**: - Update `docs/Driver.AbLegacy.Cli.md` — add an "Array reads" section explaining `N7:0,10` vs `N7:0[10]` syntax and the per-PCCC-frame ~120-word ceiling, plus a `read --array-length 10 -a N7:0,10` CLI example. - Update `docs/drivers/AbLegacy-Test-Fixture.md` — list array-block reads under unit coverage and note the latency benchmark integration test as a new perf-flagged case. - Fixture: confirm `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/docker-compose.yml` `--tag=N7[10]` / `--tag=F8[10]` already provide enough contiguous words; otherwise bump array sizes (`N7[120]` to allow max-frame tests). - E2E: extend `scripts/e2e/test-ablegacy.ps1` with a `read -a "N7:0,10"` array assertion (parse comma-separated CLI output); add a matching `IsArray=1` tag row in `scripts/smoke/seed-ablegacy-smoke.sql` to exercise the address-space side. **Effort**: M **Dependencies**: PR 1 (octal applies to array index when the file is I/O on PLC-5) --- #### PR 8 — Per-tag deadband / change filter (#18) **Scope**: Today `PollGroupEngine` publishes every poll. Add absolute and percent deadband per tag — only emit `OnDataChange` when the new value differs by ≥ deadband. **Files**: - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverOptions.cs` — `AbLegacyTagDefinition` gains `AbsoluteDeadband` (double?), `PercentDeadband` (double?). - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs` — wrap the `PollGroupEngine` callback with a per-tag last-published-value cache and the deadband test. Booleans bypass deadband (always change-on-edge). Strings + status changes always publish. - Verify: `PollGroupEngine` (in `Core.Drivers`) doesn't already centralise this — if it does, this PR threads the per-tag config through the engine instead of layering on top. **Test plan**: - Unit (new `AbLegacyDeadbandTests`): tag with `AbsoluteDeadband=1.0` reading `[10.0, 10.5, 11.5, 11.6]` publishes only `10.0` and `11.5`. Boolean tag publishes every transition. Status code change always publishes. - Quality: ensure last-value cache doesn't leak across `ReinitializeAsync`. **Docs / fixture / e2e**: - Update `docs/Driver.AbLegacy.Cli.md` — add a "Deadband" subsection under subscribe with `--deadband-absolute` / `--deadband-percent` CLI flags and example. - Update `docs/drivers/AbLegacy-Test-Fixture.md` — list `AbLegacyDeadbandTests` under unit coverage. - Fixture: no compose change required (per-tag deadband is a config-side concern, not a server simulator one). - E2E: extend `scripts/e2e/test-ablegacy.ps1` with a deadband subscribe assertion (subscribe with `--deadband-absolute 5`, write three small deltas, assert only one notification fires); add a tag row to `scripts/smoke/seed-ablegacy-smoke.sql` with `AbsoluteDeadband=5` to exercise the seed-from-config path. **Effort**: S **Dependencies**: none --- ### Phase 4 — Workflow #### PR 9 — Per-device timeout / retry overrides (#21) **Scope**: Replace single driver-wide `Timeout` with per-device override (SLC 5/01 needs ~5 s, SLC 5/05 fine at 2 s, ML1100 sometimes 3 s). Optional retry count per device. **Files**: - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverOptions.cs` — `AbLegacyDeviceOptions` gains optional `Timeout`, `Retries`. `AbLegacyDriverOptions.Timeout` becomes the driver-wide default. - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs` — `EnsureTagRuntimeAsync` and `ProbeLoopAsync` use `device.Options.Timeout ?? _options.Timeout`. `ReadAsync` retry loop honours `device.Options.Retries`. **Test plan**: - Unit: device with `Timeout=TimeSpan.FromSeconds(5)` propagates into `AbLegacyTagCreateParams.Timeout`; absent override falls back to driver-wide. - Integration: simulate a slow device (1 s artificial delay) — driver-wide 2 s passes; reducing per-device to 500 ms surfaces `BadCommunicationError` on the slow device while the fast device keeps reading. **Docs / fixture / e2e**: - Update `docs/Driver.AbLegacy.Cli.md` — document per-device `--timeout-ms` / `--retries` precedence vs driver-wide defaults; add a tuning cheat-sheet for SLC 5/01 vs 5/05 vs ML1100. - Update `docs/drivers/AbLegacy-Test-Fixture.md` — note per-device options under the AbLegacyDeviceOptions surface. - Fixture: no compose change. Add a slow-device test harness using a `tc qdisc add dev eth0 delay 1000ms` sidecar (or a Linux `iptables -j DELAY` shim) — document in `Docker/README.md` as an optional perf-tuning fixture. - E2E: no `test-ablegacy.ps1` change needed (per-device timeout is integration-test territory). Add a `Timeout=PT500MS` device-level row to `scripts/smoke/seed-ablegacy-smoke.sql` so the seed path exercises the new column. **Effort**: S **Dependencies**: none --- #### PR 10 — Diagnostic counters as tags (#20) **Scope**: Per-device diagnostic counters (request count, response count, retry count, last-error code, comm-failures) surface as auto-generated tags under `AbLegacy//_Diagnostics/*` so HMIs can bind directly. Mirrors what other drivers expose. **Files**: - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs` — `DeviceState` gains `Counters` (record of int64s). `ReadAsync`, `WriteAsync`, `ProbeLoopAsync` increment counters on success/failure paths. `DiscoverAsync` emits a `_Diagnostics` folder per device with seven Variables: `RequestCount`, `ResponseCount`, `ErrorCount`, `RetryCount`, `LastErrorCode`, `LastErrorMessage`, `CommFailures`. - New `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDiagnosticTags.cs` — generates the 7 well-known tag names; reading them returns counter snapshots from `DeviceState.Counters`. - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs` `ReadAsync` short-circuits diagnostic tag references before dispatching to libplctag. **Test plan**: - Unit (new `AbLegacyDiagnosticsTests`): force 5 reads (3 success, 2 fail) → `RequestCount=5`, `ErrorCount=2`. `LastErrorCode` reflects the last libplctag status. Counters reset on `ReinitializeAsync`. - Quality: verify the 7 well-known names don't collide with user-config tag names (reject overlap at `InitializeAsync`). **Docs / fixture / e2e**: - New doc `docs/drivers/AbLegacy-Diagnostics.md` — the seven well-known counter tag names, their semantics, namespace convention (`_Diagnostics` folder per device), reset behaviour on `ReinitializeAsync`, and HMI binding examples. - Update `docs/Driver.AbLegacy.Cli.md` — note that diagnostic tags surface alongside user-config tags and can be `read --address _Diagnostics/RequestCount` (or whatever the canonical CLI shape ends up being). - Update `docs/drivers/AbLegacy-Test-Fixture.md` — list `AbLegacyDiagnosticsTests` and call out the collision-rejection contract. - Fixture: no compose change. - E2E: extend `scripts/e2e/test-ablegacy.ps1` with a "after N reads, RequestCount==N" assertion against the diagnostic NodeId published by the OPC UA server-bridge step; add a `_Diagnostics/RequestCount` Tag row to `scripts/smoke/seed-ablegacy-smoke.sql` if the addr-space team requires explicit registration. **Effort**: M **Dependencies**: none --- #### PR 11 — RSLogix 500 / PLC-5 symbol & data-table import (#15) **Scope**: Import RSLogix exports (`.RSS` Slc500, `.RSP` Plc5, `.SLC` text export) to seed `AbLegacyTagDefinition` entries. The binary `.RSS`/`.RSP` formats are proprietary and largely undocumented; the practical strategy is to support the `.SLC` / `.CSV` text exports that RSLogix can produce ("save as text" / "Database Export"). Verify whether libplctag or a sister project ships an `.RSS` parser — if not, scope to text exports only and document the binary case as a future enhancement. **Files**: - New `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/Import/RsLogixSymbolImport.cs` — parses RSLogix text export (CSV: `Symbol,Address,Description,DataType,Scope`). - New `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/Import/IRsLogixImporter.cs` — abstraction for future binary support. - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverFactoryExtensions.cs` — extension method `AddRsLogixImport(string path, string deviceHostAddress)` materialises `AbLegacyTagDefinition` entries from the file at startup-time. - New CLI command in `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/` (mirrors AbCip CLI patterns — verify: confirm the AbLegacy CLI project layout): `import-rslogix --file foo.csv --device ab://... --emit appsettings-fragment`. **Test plan**: - Unit (new `RsLogixSymbolImportTests`): canonical CSV with one of each file letter (N/F/B/L/ST/T/C/R) generates 8 `AbLegacyTagDefinition` entries with correct `DataType`. Malformed rows skipped with logged warning. Comments and header rows skipped. - Integration: an end-to-end test with a recorded RSLogix CSV (committed under `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/Fixtures/`) produces an addr-space matching a golden snapshot. **Docs / fixture / e2e**: - New doc `docs/drivers/AbLegacy-RSLogix-Import.md` — supported export formats (CSV / .SLC text), CSV column convention, scope handling, the `import-rslogix` CLI subcommand, and the explicit non-goal of binary `.RSS`/`.RSP` parsing for v1. - Update `docs/Driver.AbLegacy.Cli.md` — add an `import-rslogix` subcommand row to the commands table with `--file foo.csv --device ab://... --emit appsettings-fragment` example. - Update `docs/DriverClis.md` if it carries a per-CLI command matrix. - Update `docs/drivers/AbLegacy-Test-Fixture.md` — list `RsLogixSymbolImportTests`, the new `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/Fixtures/` golden CSV, and the import-then-read integration scenario. - Fixture: new committed CSV under `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/Fixtures/rslogix-canonical.csv` plus the corresponding golden snapshot. No `Docker/docker-compose.yml` change. - E2E: extend `scripts/e2e/test-ablegacy.ps1` with an `import-rslogix` invocation that emits an appsettings fragment, then asserts the resulting tag count matches the CSV row count. No `seed-ablegacy-smoke.sql` change (importer is offline tooling). **Effort**: L (parser + CLI + golden-snapshot fixture) **Dependencies**: PR 1–5 complete (importer must produce addresses the parser accepts) --- ### Phase 5 — Resilience #### PR 12 — Auto-demote on comm failure (#13) **Scope**: When a device fails N consecutive reads/probes, mark it Demoted and skip its tags for `DemoteFor` seconds — so one slow PLC doesn't starve fast PLCs sharing the same driver/poll cadence. **Files**: - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverOptions.cs` — new `AbLegacyDemoteOptions { FailureThreshold=3, DemoteFor=TimeSpan.FromSeconds(30), Enabled=true }` on `AbLegacyDeviceOptions`. - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs` — `DeviceState` gains `ConsecutiveFailures`, `DemotedUntilUtc`. `ReadAsync` short-circuits demoted devices with `BadCommunicationError` until `DemotedUntilUtc`. `ProbeLoopAsync` clears demote on first success. New `HostState.Demoted` enum value (verify `HostState` is in `Core.Abstractions` and adding a member is non-breaking). - Diagnostic tags from PR 10 gain `DemoteCount` and `LastDemotedUtc`. **Test plan**: - Unit (new `AbLegacyAutoDemoteTests`): force 3 consecutive failures → device transitions to `Demoted`; reads while demoted return `BadCommunicationError` without invoking libplctag (verify via test fake counting `ReadAsync` calls). After `DemoteFor` expires, the next read attempt goes through. - Integration: two devices on the same driver, one with a fault — fault doesn't slow down the healthy one. **Docs / fixture / e2e**: - New doc `docs/drivers/AbLegacy-AutoDemote.md` (or a section appended to `AbLegacy-Diagnostics.md` from PR 10) — failure-threshold + demote-window semantics, interaction with the probe loop, the `HostState.Demoted` enum value, recovery path. - Update `docs/Driver.AbLegacy.Cli.md` — add `--demote-failure-threshold` / `--demote-for` per-device flags and document how `probe` reflects the Demoted state. - Update `docs/drivers/AbLegacy-Test-Fixture.md` — list `AbLegacyAutoDemoteTests` and the two-device fault-isolation integration case. - Fixture: extend `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/docker-compose.yml` with a second `slc500-faulty` service that listens on `:44819` but rejects every read (or doesn't bind, simulating ECONNREFUSED). The driver test then targets both `:44818` (healthy) and `:44819` (faulty) to exercise demotion. - E2E: extend `scripts/e2e/test-ablegacy.ps1` with a "kill simulator, observe demotion in `_Diagnostics/DemoteCount`" assertion (gated on PR 10's diagnostic tags being present). Add a `DemoteFor=PT30S` device row to `scripts/smoke/seed-ablegacy-smoke.sql`. **Effort**: M **Dependencies**: PR 10 (diagnostic counters) --- #### PR 13 — DH+ via 1756-DHRIO bridging (#2) **Scope**: Allow addressing a PLC-5 sitting on a DH+ link reached through a ControlLogix chassis with a 1756-DHRIO module. The CIP path syntax is `1,,2,` — already accepted as a string by `AbLegacyHostAddress`, but we should validate and document it, and verify libplctag's `plc5` PlcType resolves DH+ stations correctly through the DHRIO port. **Files**: - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyHostAddress.cs` — add validation for the DH+ path form `1,,2,` where station is 0..77 octal. Surface the parsed components (`BackplaneSlot`, `DhPlusPort`, `DhPlusStation`) for diagnostics. - `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/PlcFamilies/AbLegacyPlcFamilyProfile.cs` — note that DH+ bridging is a `Plc5`-only path (DHRIO doesn't bridge to SLC/ML). - `docs/Driver.AbLegacy.Cli.md` — add a worked example of DHRIO routing. **Test plan**: - Unit (`AbLegacyHostAndStatusTests`): `ab://10.0.0.1/1,3,2,07` parses with slot=3, station=7₈=7. `ab://10.0.0.1/1,3,2,77` parses station=77₈=63. `ab://10.0.0.1/1,3,2,80` rejects (octal range). - Integration: requires a real DHRIO + PLC-5 — flag as hardware-gated; cover with unit-only for now and document the manual smoke procedure (`docs/Driver.AbLegacy.Cli.md`). **Docs / fixture / e2e**: - New doc `docs/drivers/AbLegacy-DH-Bridging.md` — the `1,,2,` CIP path syntax, DHRIO module wiring overview, octal-station-number reference (00..77 octal = 0..63), restriction to PLC-5 family, and the manual smoke procedure since DHRIO can't be simulated. - Update `docs/Driver.AbLegacy.Cli.md` — extend the family/cip-path cheat sheet with a "PLC-5 via DHRIO" row showing `ab://logix-host/1,3,2,07` and a worked CLI example. (Plan already calls this out at line 279 — keep it, but link to the new dedicated doc.) - Update `docs/drivers/AbLegacy-Test-Fixture.md` — note that DH+ bridging is unit-only (no fixture support possible) and reference the manual hardware smoke procedure. - Fixture: no `Docker/docker-compose.yml` change is feasible (DHRIO is hardware-only). - E2E: no new automated `test-ablegacy.ps1` case (would require real DHRIO). Add a `-DhPlusStation 7` parameter form documented in the script comment header for hardware-gated runs only. No `seed-ablegacy-smoke.sql` change. **Effort**: S **Dependencies**: PR 1 (octal parsing utility) — share the octal-int helper between PR 1 and PR 13. --- ## Documentation, fixture, and e2e impact Consolidated view of every doc, fixture, and e2e/smoke artefact this plan touches, so reviewers and PR authors can size the non-code surface area at a glance. ### New docs (created by this plan) | Doc | Created by | Purpose | |-----|-----------|---------| | `docs/drivers/AbLegacy-MicroLogix-FunctionFiles.md` | PR 2 | Function-file catalogue (RTC/HSC/DLS/MMI/PTO/PWM/STI/EII/IOS/BHI), per-family availability, sub-element types | | `docs/drivers/AbLegacy-Indirect-Addressing.md` | PR 4 | `N7:[N7:0]` and `N[N7:0]:5` syntax, depth-1 limit, libplctag strategy | | `docs/drivers/AbLegacy-Structure-Files.md` | PR 5 | PD / MG / PLS / BT sub-element catalogues + per-family availability matrix | | `docs/drivers/AbLegacy-Diagnostics.md` | PR 10 | Seven well-known counter tag names, namespace convention, reset semantics | | `docs/drivers/AbLegacy-RSLogix-Import.md` | PR 11 | CSV / `.SLC` text-export schema, `import-rslogix` CLI, binary-format non-goals | | `docs/drivers/AbLegacy-AutoDemote.md` (or PR 10 doc extension) | PR 12 | Demote thresholds, recovery, `HostState.Demoted` semantics | | `docs/drivers/AbLegacy-DH-Bridging.md` | PR 13 | `1,,2,` CIP path, DHRIO wiring, manual smoke procedure | ### Updated docs (extended by this plan) - `docs/Driver.AbLegacy.Cli.md` — extended by **every** PR (octal I/O, function files, sub-element bits, indirect, structure files, ST round-trip, array reads, deadband flags, per-device timeouts, diagnostic tags, RSLogix import subcommand, demote flags, DHRIO cheat-sheet row). - `docs/drivers/AbLegacy-Test-Fixture.md` — extended by **every** PR with new unit test classes, integration cases, and fixture limitations. - `docs/DriverClis.md` — touched by PR 11 (new `import-rslogix` subcommand row). - `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/README.md` — touched by PRs 1, 2, 4, 5, 9, 12 (fixture limitations, optional perf-tuning sidecars, faulty-device service, recipe-pattern note). ### Fixture / scaffolding work - `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/docker-compose.yml`: - PR 1: extend `plc5` profile with `I:001`-style tags (if `ab_server` accepts). - PR 2: extend `micrologix` profile with `RTC0[1]`/`HSC0[1]` (if accepted). - PR 3: extend `slc500` profile with `T4[5]`/`C5[5]`/`R6[5]` if not already seeded by `ab_server` defaults. - PR 5: extend `slc500` and `plc5` profiles with `PD9[2]`/`MG10[2]` (if accepted). - PR 6: extend `slc500` and `plc5` profiles with `ST20[5]`. - PR 7: bump array sizes (`N7[120]`) for max-frame array-read tests. - PR 12: add a second `slc500-faulty` service for demotion/fault-isolation tests. - `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/Fixtures/`: - PR 11: new `rslogix-canonical.csv` + golden snapshot for the symbol-import integration test. ### E2E / smoke scripts - `scripts/e2e/test-ablegacy.ps1`: - PR 1: octal-bit `Plc5` assertion. - PR 2: `MicroLogix RTC:0.HR` parametric. - PR 3: Boolean sub-element read (`T4:0.DN`). - PR 4: indirect-address loopback. - PR 5: `PD9:0.SP` Float read (skip-gated). - PR 6: ST round-trip. - PR 7: array-read `N7:0,10`. - PR 8: deadband subscribe assertion. - PR 10: `_Diagnostics/RequestCount` assertion via OPC UA bridge. - PR 11: `import-rslogix` invocation + tag-count assertion. - PR 12: kill-simulator-and-observe-demote assertion. - PR 13: parameter-only header note for hardware-gated DHRIO runs. - `scripts/smoke/seed-ablegacy-smoke.sql`: - PR 3: `T4:0.DN` Boolean tag row. - PR 5: `PD9:0.SP` PidElement tag row (skip-gated). - PR 6: `ST20:0` String tag row. - PR 7: `N7:0,10` array tag row (`IsArray=1`). - PR 8: tag row with `AbsoluteDeadband=5`. - PR 9: device row with `Timeout=PT500MS`. - PR 10: `_Diagnostics/RequestCount` tag row (if explicit registration required). - PR 12: device row with `DemoteFor=PT30S`. --- ## Skip-rated items (for context) For traceability, the gaps the recommendations table flagged **No**: | # | Gap | Skip rationale | |---|-----|----------------| | 1 | Serial DF1 transports (full-duplex, half-duplex, KF2/KF3) | libplctag has no serial path; declining install base | | 3 | DH-485 routing (1761/1747-AIC) | Very legacy; rare in greenfield | | 4 | M0 / M1 module file access | Niche RIO modules; declining | | 6 | D (BCD) and Long-BCD types | Very legacy data convention | | 12 | Block read-size negotiation per family | libplctag handles chunking implicitly | | 14 | Channel-shared comm serialisation | Only matters for serial / DH+ transport (not built) | | 16 | Online controller browse / data-table discovery | PCCC dir frame limited; libplctag support unclear | | 17 | DF1 BCC vs CRC-16 selection | Predicated on DF1 transport (gap #1) | | 19 | PLC-5 typed-read selection / Force Logical | libplctag defaults are sound; niche tuning | | 22 | Write completion semantics options | Niche tuning; current write-through is safe default | These remain documented in `featuregaps.md` and can be reopened if customer feedback warrants. --- ## Open questions 1. **libplctag PCCC capability verification** — several PRs (especially 2, 4, 5, 7) hinge on what libplctag's `slc500` / `micrologix` / `plc5` / `logixpccc` PlcTypes actually accept in the `Name` attribute. Before scheduling Phase 2 we should run a one-day spike with the AbLegacy simulator to confirm: - Does libplctag accept indirect addresses (`N7:[N7:0]`) verbatim, or do we need to resolve in two steps? - Does it accept array notation (`N7:0,10` vs `N7:0[10]`) for PCCC PlcTypes? - Does it expose PD/MG/PLS/BT sub-elements by name, or do we read the parent struct as a byte block? - Does it correctly handle PLC-5 octal in I:/O: addresses, or does the driver need to convert? 2. **MicroLogix simulator fidelity** — we don't currently know whether the AbLegacy integration-test fixture (`AbLegacyServerFixture`) simulates the MicroLogix function files (RTC/HSC/DLS). PR 2's integration coverage is gated on this. If not, we either extend the fixture or scope PR 2 to unit-only tests + a hardware smoke-test playbook. 3. **RSLogix import format coverage** — binary `.RSS` / `.RSP` parsing is non-trivial. PR 11 scopes to text/CSV exports. Should we instead invest in shelling out to the (free) Rockwell `RSWho` / `RSLogix Emulate` tooling for binary conversion, or accept text-only as the v1 scope and revisit? 4. **Address-space rebuild on tag-set change** — when PR 11 (RSLogix import) adds 1000+ tags, does `ReinitializeAsync` perform acceptably, or do we need an incremental discovery path? Out of scope for this plan but worth flagging. 5. **Diagnostic tag namespace collision** — PR 10 reserves `_Diagnostics` under each device folder. Confirm with the address-space team that the leading underscore is the established convention (other drivers use `_System` or `_DiagnosticTags`); align before implementation.