Captures uncommitted work that lived in the working tree on
v2-mxgw-integration but was orthogonal to the migration. Stashed
during the v2-mxgw merge to master (2026-04-30) and replanted here on
a feature branch off master so it's git-visible rather than living in
the stash list.
Two distinct buckets:
1. Tracked fixture/config refinements (10 files, ~36 lines):
- scripts/e2e/test-opcuaclient.ps1
- src/ZB.MOM.WW.OtOpcUa.Admin/appsettings.json
- 5 docker-compose.yml under tests/.../IntegrationTests/Docker/
(AbCip, Modbus, OpcUaClient, S7)
- 4 fixture .cs files (AbServerFixture, ModbusSimulatorFixture,
OpcPlcFixture, Snap7ServerFixture)
2. Untracked driver-gaps queue artifacts (~8000 lines):
- docs/plans/{abcip,ablegacy,focas,opcuaclient,s7,twincat}-plan.md
— per-driver gap plans
- docs/featuregaps.md — cross-cutting analysis
- docs/v2/focas-deployment.md, docs/v2/implementation/focas-simulator-plan.md
- followup.md — auto/driver-gaps queue follow-ups
- scripts/queue/ — PR-queue automation tooling (12 files including
pr-manifest.yaml at 1473 lines)
This commit is a snapshot for recoverability — review and split into
focused PRs (or discard) before merging anywhere downstream.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
41 KiB
AbLegacy Driver — Implementation Plan
Source of gap analysis: featuregaps.md → AbLegacy
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:
- DH+ via 1756-DHRIO bridging (#2)
- PD/MG/PLS/BT files (#5)
- PLC-5 octal addressing (#7)
- Indirect/indexed addressing (#8)
- Array contiguous block addressing (#9)
- ST string read/write production verification (#10)
- Sub-element bit semantics (
.DNas Bit) (#11) - Auto-demote on comm failure (#13)
- RSLogix 500/5 symbol import (#15)
- Per-tag deadband / change filter (#18)
- Diagnostic counters as tags (#20)
- Per-device timeout / retry overrides (#21)
- 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— addTryParse(string, AbLegacyPlcFamily)overload; existingTryParse(string)keeps decimal semantics (back-compat for non-PLC-5 callers and pure shape validation).src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs—EnsureTagRuntimeAsyncand the bit-RMW path call the family-aware overload usingdevice.Options.PlcFamily.src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/PlcFamilies/AbLegacyPlcFamilyProfile.cs— addOctalIoAddressingflag (true forPlc5only).
Test plan:
- Unit (
tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyAddressTests.cs):I:001/17parses 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 anI:/O:row noting PLC-5 octal vs SLC500 decimal semantics; worked example showingI:001/17resolved 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.ymlplc5profile to seed anI:001(or equivalent module-image word) tag ifab_server --plc=PLC/5accepts it; otherwise document the gap inDocker/README.md. - E2E: add
--plc-type Plc5 -a "I:001/17"octal-bit assertion toscripts/e2e/test-ablegacy.ps1(gated on theplc5compose profile being up); no change toscripts/smoke/seed-ablegacy-smoke.sqlrequired (existingN7:5tag 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— extendIsKnownFileLetterto recognise multi-letter function-file types (RTC,HSC,DLS,MMI,PTO,PWM,STI,EII,IOS,BHI). Permit only when family isMicroLogix. 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 rightDriverDataType.src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/PlcFamilies/AbLegacyPlcFamilyProfile.cs—SupportsFunctionFilesflag.
Test plan:
- Unit:
RTC:0.HRparses withFileLetter="RTC",WordNumber=0,SubElement="HR".HSC:0.ACCparses. 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 libplctagmicrologixPlcType 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 withRTC:0.HR/HSC:0.ACCexamples and a CLI worked example. - Update
docs/drivers/AbLegacy-Test-Fixture.md— record fixture coverage status for function files and link to themicrologixprofile gap (only ifab_server --plc=Micrologixrejects function-file addresses, document the unit-only fallback). - Fixture: extend
tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/docker-compose.ymlmicrologixprofile with--tag=RTC0[1]/--tag=HSC0[1]if accepted byab_server, else mark as hardware-gated inDocker/README.md. - E2E: add a parametric
-PlcType MicroLogix -Address RTC:0.HRinvocation toscripts/e2e/test-ablegacy.ps1(skip-when-fixture-gap, mirroring the existingBadCommunicationErrorgate); noseed-ablegacy-smoke.sqlchange 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 helperSubElementBitNames(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). NewEffectiveDriverDataType(AbLegacyDataType, string? subElement)returningBooleanfor bit-typed sub-elements, otherwise the existing mapping.src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs—DiscoverAsyncusesEffectiveDriverDataType(def.DataType, parsed.SubElement);ReadAsyncdecodes 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.DNetc. as a single bit when read withGetBitagainst the sub-element address. If not, fall back to read-the-word + mask.
Test plan:
- Unit (
AbLegacyDriverTests+ newAbLegacyDataTypeTests):T4:0.DNdiscovers as Boolean;T4:0.ACCdiscovers as Int32; counter.OVis Boolean; control.LENis Int32. - Bit-write semantics: writing Boolean
truetoT4:0.DNshould be rejected withBadNotWritable(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.DNCLI example. - Update
docs/drivers/AbLegacy-Test-Fixture.md— noteAbLegacyDataTypeTestsas a new unit-coverage class under "What it actually covers". - Fixture: no compose change required (T4/C5/R6 already seeded by
ab_serverdefaults — verify; if not, add--tag=T4[5]/--tag=C5[5]/--tag=R6[5]to theslc500profile inDocker/docker-compose.yml). - E2E: extend
scripts/e2e/test-ablegacy.ps1with a Boolean sub-element read assertion (read --type Bool -a T4:0.DN) once the simulator round-trip works. Updatescripts/smoke/seed-ablegacy-smoke.sqlto add a Boolean tag bindingT4:0.DNso 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 nullableIndirectFileSourceandIndirectWordSource(each itself anAbLegacyAddress). Parser handles[<inner>]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/plc5PlcType accepts aNameof formN7:[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— explainN7:[N7:0]andN[N7:0]:5syntax, 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, listAbLegacyAddressTestsindirect-parsing cases. - Fixture: no
Docker/docker-compose.ymlchange required (N7[10]already seeded; the inner index tag atN7:0is already addressable). Document recipe-pattern inDocker/README.md. - E2E: extend
scripts/e2e/test-ablegacy.ps1with an indirect-address driver-loopback case (write toN7:0to set the index, then readN7:[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— extendIsKnownFileLetterwithPD,MG,PLS,BT.src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDataType.cs— new enum membersPidElement,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—SupportsPidFileetc. 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; rejectPD9: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 PidElementetc. 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 byab_server). - Fixture: extend
tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/docker-compose.ymlslc500andplc5profiles with--tag=PD9[2]/--tag=MG10[2]ifab_serveraccepts; otherwise document gap inDocker/README.mdand rely on unit coverage. - E2E: extend
scripts/e2e/test-ablegacy.ps1with aread --type Float -a PD9:0.SPassertion when fixture exposes the file; add a corresponding tag row toscripts/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'sGetString/SetStringalready handles the length-word convention; if not, addGetByteArrayBuffer+ manual length-word decode.tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyReadSmokeTests.cs— addST_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 (mockIAbLegacyTagRuntimereturning 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
BadOutOfRangesurfaces when caller writes a 100-char string to an 82-byte ST.
Docs / fixture / e2e:
- Update
docs/Driver.AbLegacy.Cli.md— expand theSTrow in the address primer with the 82-byte limit, length-word convention, and awrite --type String --value "Hello"worked example. - Update
docs/drivers/AbLegacy-Test-Fixture.md— list the newAbLegacyStringEncodingTestsunit class and the fourST_RoundTrip_*integration cases under coverage. - Fixture: extend
tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/docker-compose.ymlslc500andplc5profiles with--tag=ST20[5]so the round-trip tests have a real address to write against; document anyab_serverST gaps inDocker/README.md. - E2E: extend
scripts/e2e/test-ablegacy.ps1with a String round-trip case (-a "ST20:0" --type String) and aStringtag row inscripts/smoke/seed-ablegacy-smoke.sqlso 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 gainsArrayCount(nullable). Parser accepts,Nsuffix (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—AbLegacyTagDefinitiongains optionalArrayLength(overrides parsed value; convenient when address is parameterised).src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/IAbLegacyTagRuntime.cs—AbLegacyTagCreateParamsgainsElementCount(default 1).src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs— passElementCountto libplctagTag.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—DiscoverAsyncemitsIsArray=true,ArrayDim=[N];ReadAsyncdecodes via per-index_tag.GetInt16(i*2)etc.
Test plan:
- Unit:
N7:0,10parses ArrayCount=10;N7:0[10]same;N7:0,10/3rejects (array+bit);T4:0,5.ACCrejects (array+sub-element). - Integration: read
N7:0,10returns 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 explainingN7:0,10vsN7:0[10]syntax and the per-PCCC-frame ~120-word ceiling, plus aread --array-length 10 -a N7:0,10CLI 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.ps1with aread -a "N7:0,10"array assertion (parse comma-separated CLI output); add a matchingIsArray=1tag row inscripts/smoke/seed-ablegacy-smoke.sqlto 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—AbLegacyTagDefinitiongainsAbsoluteDeadband(double?),PercentDeadband(double?).src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs— wrap thePollGroupEnginecallback 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(inCore.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 withAbsoluteDeadband=1.0reading[10.0, 10.5, 11.5, 11.6]publishes only10.0and11.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-percentCLI flags and example. - Update
docs/drivers/AbLegacy-Test-Fixture.md— listAbLegacyDeadbandTestsunder 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.ps1with a deadband subscribe assertion (subscribe with--deadband-absolute 5, write three small deltas, assert only one notification fires); add a tag row toscripts/smoke/seed-ablegacy-smoke.sqlwithAbsoluteDeadband=5to 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—AbLegacyDeviceOptionsgains optionalTimeout,Retries.AbLegacyDriverOptions.Timeoutbecomes the driver-wide default.src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs—EnsureTagRuntimeAsyncandProbeLoopAsyncusedevice.Options.Timeout ?? _options.Timeout.ReadAsyncretry loop honoursdevice.Options.Retries.
Test plan:
- Unit: device with
Timeout=TimeSpan.FromSeconds(5)propagates intoAbLegacyTagCreateParams.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
BadCommunicationErroron the slow device while the fast device keeps reading.
Docs / fixture / e2e:
- Update
docs/Driver.AbLegacy.Cli.md— document per-device--timeout-ms/--retriesprecedence 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 1000mssidecar (or a Linuxiptables -j DELAYshim) — document inDocker/README.mdas an optional perf-tuning fixture. - E2E: no
test-ablegacy.ps1change needed (per-device timeout is integration-test territory). Add aTimeout=PT500MSdevice-level row toscripts/smoke/seed-ablegacy-smoke.sqlso 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/<host>/_Diagnostics/* so HMIs can bind directly. Mirrors what other drivers expose.
Files:
src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs—DeviceStategainsCounters(record of int64s).ReadAsync,WriteAsync,ProbeLoopAsyncincrement counters on success/failure paths.DiscoverAsyncemits a_Diagnosticsfolder 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 fromDeviceState.Counters. src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.csReadAsyncshort-circuits diagnostic tag references before dispatching to libplctag.
Test plan:
- Unit (new
AbLegacyDiagnosticsTests): force 5 reads (3 success, 2 fail) →RequestCount=5,ErrorCount=2.LastErrorCodereflects the last libplctag status. Counters reset onReinitializeAsync. - 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 (_Diagnosticsfolder per device), reset behaviour onReinitializeAsync, and HMI binding examples. - Update
docs/Driver.AbLegacy.Cli.md— note that diagnostic tags surface alongside user-config tags and can beread --address _Diagnostics/RequestCount(or whatever the canonical CLI shape ends up being). - Update
docs/drivers/AbLegacy-Test-Fixture.md— listAbLegacyDiagnosticsTestsand call out the collision-rejection contract. - Fixture: no compose change.
- E2E: extend
scripts/e2e/test-ablegacy.ps1with a "after N reads, RequestCount==N" assertion against the diagnostic NodeId published by the OPC UA server-bridge step; add a_Diagnostics/RequestCountTag row toscripts/smoke/seed-ablegacy-smoke.sqlif 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 methodAddRsLogixImport(string path, string deviceHostAddress)materialisesAbLegacyTagDefinitionentries 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 8AbLegacyTagDefinitionentries with correctDataType. 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, theimport-rslogixCLI subcommand, and the explicit non-goal of binary.RSS/.RSPparsing for v1. - Update
docs/Driver.AbLegacy.Cli.md— add animport-rslogixsubcommand row to the commands table with--file foo.csv --device ab://... --emit appsettings-fragmentexample. - Update
docs/DriverClis.mdif it carries a per-CLI command matrix. - Update
docs/drivers/AbLegacy-Test-Fixture.md— listRsLogixSymbolImportTests, the newtests/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.csvplus the corresponding golden snapshot. NoDocker/docker-compose.ymlchange. - E2E: extend
scripts/e2e/test-ablegacy.ps1with animport-rslogixinvocation that emits an appsettings fragment, then asserts the resulting tag count matches the CSV row count. Noseed-ablegacy-smoke.sqlchange (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— newAbLegacyDemoteOptions { FailureThreshold=3, DemoteFor=TimeSpan.FromSeconds(30), Enabled=true }onAbLegacyDeviceOptions.src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs—DeviceStategainsConsecutiveFailures,DemotedUntilUtc.ReadAsyncshort-circuits demoted devices withBadCommunicationErroruntilDemotedUntilUtc.ProbeLoopAsyncclears demote on first success. NewHostState.Demotedenum value (verifyHostStateis inCore.Abstractionsand adding a member is non-breaking).- Diagnostic tags from PR 10 gain
DemoteCountandLastDemotedUtc.
Test plan:
- Unit (new
AbLegacyAutoDemoteTests): force 3 consecutive failures → device transitions toDemoted; reads while demoted returnBadCommunicationErrorwithout invoking libplctag (verify via test fake countingReadAsynccalls). AfterDemoteForexpires, 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 toAbLegacy-Diagnostics.mdfrom PR 10) — failure-threshold + demote-window semantics, interaction with the probe loop, theHostState.Demotedenum value, recovery path. - Update
docs/Driver.AbLegacy.Cli.md— add--demote-failure-threshold/--demote-forper-device flags and document howprobereflects the Demoted state. - Update
docs/drivers/AbLegacy-Test-Fixture.md— listAbLegacyAutoDemoteTestsand the two-device fault-isolation integration case. - Fixture: extend
tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/docker-compose.ymlwith a secondslc500-faultyservice that listens on:44819but 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.ps1with a "kill simulator, observe demotion in_Diagnostics/DemoteCount" assertion (gated on PR 10's diagnostic tags being present). Add aDemoteFor=PT30Sdevice row toscripts/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,<slot>,2,<dh+_station_octal> — 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 form1,<slot>,2,<station>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 aPlc5-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,07parses with slot=3, station=7₈=7.ab://10.0.0.1/1,3,2,77parses station=77₈=63.ab://10.0.0.1/1,3,2,80rejects (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— the1,<slot>,2,<station_octal>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 showingab://logix-host/1,3,2,07and 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.ymlchange is feasible (DHRIO is hardware-only). - E2E: no new automated
test-ablegacy.ps1case (would require real DHRIO). Add a-DhPlusStation 7parameter form documented in the script comment header for hardware-gated runs only. Noseed-ablegacy-smoke.sqlchange.
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,<slot>,2,<station_octal> 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 (newimport-rslogixsubcommand 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
plc5profile withI:001-style tags (ifab_serveraccepts). - PR 2: extend
micrologixprofile withRTC0[1]/HSC0[1](if accepted). - PR 3: extend
slc500profile withT4[5]/C5[5]/R6[5]if not already seeded byab_serverdefaults. - PR 5: extend
slc500andplc5profiles withPD9[2]/MG10[2](if accepted). - PR 6: extend
slc500andplc5profiles withST20[5]. - PR 7: bump array sizes (
N7[120]) for max-frame array-read tests. - PR 12: add a second
slc500-faultyservice for demotion/fault-isolation tests.
- PR 1: extend
tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/Fixtures/:- PR 11: new
rslogix-canonical.csv+ golden snapshot for the symbol-import integration test.
- PR 11: new
E2E / smoke scripts
scripts/e2e/test-ablegacy.ps1:- PR 1: octal-bit
Plc5assertion. - PR 2:
MicroLogix RTC:0.HRparametric. - PR 3: Boolean sub-element read (
T4:0.DN). - PR 4: indirect-address loopback.
- PR 5:
PD9:0.SPFloat read (skip-gated). - PR 6: ST round-trip.
- PR 7: array-read
N7:0,10. - PR 8: deadband subscribe assertion.
- PR 10:
_Diagnostics/RequestCountassertion via OPC UA bridge. - PR 11:
import-rslogixinvocation + tag-count assertion. - PR 12: kill-simulator-and-observe-demote assertion.
- PR 13: parameter-only header note for hardware-gated DHRIO runs.
- PR 1: octal-bit
scripts/smoke/seed-ablegacy-smoke.sql:- PR 3:
T4:0.DNBoolean tag row. - PR 5:
PD9:0.SPPidElement tag row (skip-gated). - PR 6:
ST20:0String tag row. - PR 7:
N7:0,10array tag row (IsArray=1). - PR 8: tag row with
AbsoluteDeadband=5. - PR 9: device row with
Timeout=PT500MS. - PR 10:
_Diagnostics/RequestCounttag row (if explicit registration required). - PR 12: device row with
DemoteFor=PT30S.
- PR 3:
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
- libplctag PCCC capability verification — several PRs (especially 2, 4, 5, 7) hinge on what libplctag's
slc500/micrologix/plc5/logixpcccPlcTypes actually accept in theNameattribute. 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,10vsN7: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?
- Does libplctag accept indirect addresses (
- 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. - RSLogix import format coverage — binary
.RSS/.RSPparsing is non-trivial. PR 11 scopes to text/CSV exports. Should we instead invest in shelling out to the (free) RockwellRSWho/RSLogix Emulatetooling for binary conversion, or accept text-only as the v1 scope and revisit? - Address-space rebuild on tag-set change — when PR 11 (RSLogix import) adds 1000+ tags, does
ReinitializeAsyncperform acceptably, or do we need an incremental discovery path? Out of scope for this plan but worth flagging. - Diagnostic tag namespace collision — PR 10 reserves
_Diagnosticsunder each device folder. Confirm with the address-space team that the leading underscore is the established convention (other drivers use_Systemor_DiagnosticTags); align before implementation.