Task 22 (spec section 8 P5), with the controller rulings that exclude PLC-5 and LogixPccc. - Transport is nullable on both option records; null (key absent) means "use the default" and is never serialized. Factory DTO parse: absent -> null; a bad value still fails via ParseEnum. - One resolver per driver (AbCipTransportResolver / AbLegacyTransportResolver), used by the driver constructor, the Test Connect probe and (through the driver) the CLI. AbCip: null -> Native. AbLegacy: null -> Native unless any device is Plc5 or LogixPccc, then the whole driver resolves to Libplctag. Explicit values always win; explicit Native with a Plc5/LogixPccc device logs one Warning at init. - Init log shows configured vs effective, e.g. transport=Native (default). - AdminUI forms: "Default (...)" / "Native (managed)" / "libplctag (fallback)". Default omits the key on save (reverses Task 19's explicit re-emit, which would have pinned every edited driver). - CLIs: --transport default|native|libplctag, default "default" via the resolver, so --plc-type Plc5 with no --transport runs libplctag. - Tests pinned to Libplctag where a default-path runtime was created: found by instrumenting the native factories with the current xUnit test name (one unit test, AbLegacyReinitConfigAdoption; plus the two fixture-gated AbCip probe E2E tests whose exact messages were proven on libplctag). TransportMatrix and the differential harness already pass explicit transports. - Switch/resolver/CLI/form tests are falsified by mutating the resolver (default -> Libplctag, exclusion removed, AbLegacy default -> Libplctag): each mutation turns them red. - Replaces the inline role-name literal in AbCipDriverFactoryExtensions' ParseEnum call with a named constant; RoleAuthorityGuardTests was already red on the base commit for that line. - Docs: AbCip.md / AbLegacy.md default + release note; CLAUDE.md AB section. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
13 KiB
AB Legacy Driver
In-process native-protocol driver that exposes legacy Allen-Bradley PLCs —
SLC 500, MicroLogix, PLC-5, and Logix-via-PCCC — as OPC UA nodes. It
runs inside the OtOpcUa server's .NET 10 AnyCPU process and speaks PCCC over
EtherNet/IP through the same libplctag.NET wrapper as the AB CIP driver, but
addresses data by file (data-table) rather than by symbolic tag. One driver
instance can serve many devices; per-device routing is keyed on the canonical
ab://gateway[:port]/cip-path host-address string. PCCC has no native push
model, so subscriptions are a polling overlay on top of IReadable.
For the driver spec (capability surface, config shape, payload limits), see docs/v2/driver-specs.md §4. For the manual test client, see Driver.AbLegacy.Cli.md. For the integration fixture coverage map, see AbLegacy-Test-Fixture.md.
Project Layout
| Project | Role |
|---|---|
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/ |
The driver — AbLegacyDriver, the libplctag runtime wrapper, the PCCC file-address parser (AbLegacyAddress), the host-address parser, and the status mapper. |
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/ |
AbLegacyDriverOptions, AbLegacyDeviceOptions, AbLegacyTagDefinition, and the AbLegacyDataType / AbLegacyPlcFamily / AbLegacyPlcFamilyProfile records bound from the driver's DriverConfig JSON. |
Per family the AbLegacyPlcFamilyProfile supplies the libplctag plc attribute,
default CIP path, max-payload bytes, and the SupportsStringFile /
SupportsLongFile capability flags. MicroLogix uses direct EIP (empty default
path); MicroLogix and PLC-5 don't ship L-files; PLC-5 predates them entirely.
Tag types are validated against the device's profile at init time — declaring a
Long or String tag on a family that can't support it fails fast with a clear
message.
Capability Surface
AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IDisposable, IAsyncDisposable
(Driver.AbLegacy/AbLegacyDriver.cs). There is no IAlarmSource — unlike the
AB CIP driver, PCCC has no ALMD instruction to project, so alarms are out of
scope.
| Capability | Implementation entry point | Notes |
|---|---|---|
ITagDiscovery |
DiscoverAsync |
Emits pre-declared tags under per-device folders. Tags are single-element today (IsArray hard-wired false); multi-element file ranges are a tracked follow-up. |
IReadable |
ReadAsync |
Per-tag reads serialized per cached runtime under a lock (a libplctag Tag handle is not concurrency-safe across the server read path + poll loop). |
IWritable |
WriteAsync |
Bit-within-word writes (N/L-file N7:0/3, B-file B3:0/0, and I/O image files I:0/0/O:1/2) do a per-parent-word read-modify-write under a lock. An Input-image (I) write is forwarded to the PLC and may be rejected by the device (the device drives the input image from physical inputs); the driver surfaces the device's real PCCC status in that case. Non-writable tags return BadNotWritable. |
ISubscribable |
SubscribeAsync driven by the shared PollGroupEngine |
No push model — subscriptions become polling groups. |
IHostConnectivityProbe |
ProbeLoopAsync + GetHostStatuses |
One probe loop per device reading Probe.ProbeAddress; transitions log Warning (down) / Information (recover). |
IPerCallHostResolver |
ResolveHost |
Routes each call to the tag's DeviceHostAddress; unknown references fall back to the first device, never throwing (per the interface contract). |
Addressing Model
Per-device host addresses are the canonical ab://gateway[:port]/cip-path form
parsed by AbLegacyHostAddress.TryParse. When the parsed CIP path is empty the
family profile's default path is used (e.g. SLC 500 gets 1,0; MicroLogix stays
empty for direct EIP).
Tags carry a PCCC file address parsed by AbLegacyAddress (AbLegacyAddress.cs)
— file letter + file number + word number, with an optional bit index (/N) or
structured sub-element (.ACC, .PRE, …). The string is passed straight through
to libplctag's name= attribute; the parser validates shape and surfaces the
pieces for driver-side routing (e.g. deciding a bit needs read-modify-write):
| Form | Meaning |
|---|---|
N7:0 |
Integer file 7, word 0 (signed 16-bit) |
F8:0 |
Float file 8, word 0 (32-bit IEEE-754) |
B3:0/0 |
Bit file 3, word 0, bit 0 |
L9:0 |
Long-integer file (SLC 5/05+, 32-bit) |
ST9:0 |
String file (82-byte fixed-length) |
T4:0.ACC / C5:0.PRE |
Timer / counter sub-element |
I:0/0 / O:1/2 / S:1 |
Input / output / status system files (no file number) |
AbLegacyDataType covers the corresponding PCCC types: Bit, Int (N), Long
(L), Float (F), AnalogInt (A), String (ST), and the TimerElement /
CounterElement / ControlElement sub-element families. The parser enforces
PCCC structural rules — bit-addressing only on 16/32-bit element files,
sub-elements only on T/C/R files, no file number on I/O/S — rejecting malformed
addresses before they reach libplctag.
Bit-within-word writes — B-file (B3:0/0), I/O image (I:0/0, O:1/2),
N-file (N7:0/3), and L-file bit addresses all use the same parent-word
read-modify-write (RMW) path: the driver reads the parent word, flips the target
bit, and writes the word back, under a per-parent lock. The lock serialises the
driver's own concurrent bit-writers against the same word, but cannot guard
against the PLC program writing that word between the driver's read and write —
this is an inherent limitation of any software-level RMW. Input-image (I) bit
writes are forwarded to the PLC as-is; the device drives the input image from
physical inputs and may reject the write.
Configuration
AbLegacyDriverOptions (Driver.AbLegacy.Contracts/AbLegacyDriverOptions.cs)
binds from the driver's DriverConfig JSON:
Devices— oneAbLegacyDeviceOptionsper PLC (HostAddress,PlcFamily, optionalDeviceName).Tags— pre-declaredAbLegacyTagDefinitionlist (Name,DeviceHostAddress,Address,DataType,Writable,WriteIdempotent).Probe— connectivity-probeEnabled/Interval/Timeout/ProbeAddress.Transport— absent (default ⇒Native, butLibplctagwhen any device is PLC-5 /LogixPccc),Native, orLibplctag(fallback); see Transport.
Full per-field descriptions live in the contracts assembly. The JSON skeleton is reproduced in docs/v2/driver-specs.md §4.
Transport (transport option)
Same switch as AB CIP: transport = absent | Native | Libplctag, a string (case-insensitive on read,
written as "Libplctag" / "Native"; "use the default" is encoded by omitting the key). Native is a managed
PCCC-over-EtherNet/IP client (Driver.Ab.Transport).
Default resolution (one place, AbLegacyTransportResolver, used by the driver, the Test Connect probe and
the CLI): an absent key resolves to Native, unless any configured device has PlcFamily Plc5 or
LogixPccc — then the whole driver resolves to Libplctag, because the transport is chosen once per driver,
not per device. An explicit value always wins: explicit Native with a PLC-5 / LogixPccc device is allowed
(an operator choice) and logs one Warning at init naming the limitation.
A change takes effect when the driver instance is recreated (a deploy that changes DriverConfig). The
AdminUI form's Transport dropdown offers "Default (Native; libplctag for PLC-5 / LogixPccc)" (omits the
key on save), "Native (managed)" and "libplctag (fallback)". The driver logs
transport=<effective> (default|configured) on every init, e.g. transport=Libplctag (default; PLC-5/LogixPccc device present). CLI: --transport default|native|libplctag (default default, so --plc-type Plc5 with no
--transport runs libplctag). See
AbCip.md §Transport for the shared behaviour (one pooled connection
per device, Forward Open throttle, routing).
Native routing: SLC500/MicroLogix/PLC-5 are direct; logixpccc uses UnconnectedSend and its SLC framing is
unverified.
Release note — native is the default (Task 22)
- The native managed transport is now the default for SLC 500 and MicroLogix. A driver config with no
transportkey runsNative; libplctag remains available as the fallback. - Exclusion: a defaulted driver with any PLC-5 or
LogixPcccdevice stays on libplctag. Native PLC-5 support is N-file reads only, andLogixPcccframing is unverified. ExplicitNativeis still honoured there, with a Warning at init. - ⚠️ Client-visible: native reports
BadTypeMismatchfor a mis-authored same-width type, where libplctag returned Good garbage (see AbCip.md §Native transport behaviour). - To fall back, set
"transport": "Libplctag"in the driver config. - When it takes effect: when a deploy recreates the driver instance (not on
ReinitializeAsync). An existing driver with no key moves to Native on its next recreate. - Evidence, and its limits: all of AbLegacy is simulator-proven only (
ab_server). No real SLC, MicroLogix or PLC-5 has been exercised. (The real-press gate, Z-35162 2026-10-08, covered AbCip reads only.)
Native known gaps
- ⚠️ Simulator-only. PCCC reads and writes are verified against
ab_serveronly. No real SLC/MicroLogix/ PLC-5 has been exercised; the real-press gate (Task 21) was read-only and AbCip-only. - PLC-5 native is N-file reads only. Every other file type and all PLC-5 writes are refused, currently
surfacing as
BadCommunicationErrorrather thanBadNotSupported(#571). The default excludes PLC-5 andlogixpccc(they resolve to libplctag unlessNativeis set explicitly). - Status-bit sub-element writes (e.g.
T4:0/DN,C5:0/DN) are Unsupported natively (they would clobber the control word); nothing goes on the wire. Sub-element arrays are Unsupported natively. L-file bit indexes above 15 are allowed. - ST (string) file layout is uncaptured, so the string byte order is unverified natively.
- Capture recipe for golden frames:
scripts/ab-capture/capture.sh(see AbLegacy-Test-Fixture.md). Follow-up issues: #564 to #577.
Testing
- Unit tests —
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/cover the driver, the PCCC address parser, and the host-address parser via fake tag runtimes. - Integration tests —
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/run against the AB Legacy Docker fixture. See AbLegacy-Test-Fixture.md for the coverage map. - Manual client — Driver.AbLegacy.Cli.md.
1-D array support
An AB Legacy tag becomes a 1-D OPC UA array node when its TagConfig JSON carries
"isArray": true and "arrayLength": N (N ≥ 1, capped at 256). The canonical rule:
isArray: true + arrayLength >= 1 → array; isArray: false (any length) → scalar.
Read mechanism — the driver issues a PCCC multi-element file read via libplctag.
The tag's file address (e.g. N7:0) is extended by N element count so libplctag fetches
N consecutive PCCC file elements in one EIP/PCCC request. The response is decoded into
N values of the declared AbLegacyDataType. Maximum element count is 256 (PCCC payload
limit).
This closes the previously-noted limitation ("single-element addressing today") for the array read path. Array writes remain a follow-up.
Unit test coverage — tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/ covers
the multi-element read path via the fake tag runtime. The AB Legacy integration fixture is
down during normal dev.
Live-verify — integration-fixture-gated (requires the AB Legacy Docker fixture).
Deferrals — array writes, multi-dimensional arrays, per-element historization.
arrayLength > 256 is rejected at init with an explicit error (PCCC payload cap).
See Uns.md §Array tags for the cross-driver coverage matrix and the UI authoring flow.
Operational Notes
- Native heap is invisible to the GC. As with AB CIP, libplctag's native heap does not show up in CLR allocation figures; watch whole-process RSS and use
ReinitializeAsyncto recycle libplctag handles. (IDriver.GetMemoryFootprint()was deleted as unconsumed in Gitea #525.) - PCCC reconnect is more expensive than CIP — legacy PLCs have no connection multiplexing, so the resilience pipeline should use longer backoff than for AB CIP (see docs/v2/driver-specs.md §4).
- Single-element addressing today — a PCCC file is inherently an array (an N7 file is up to 256 words), but the current tag surface addresses one element per tag; range-spanning tags must be enumerated element-by-element until multi-element addressing lands.