Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
15 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 ⇒Libplctag),Native(opt-in), orLibplctag; 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 Libplctag for every PLC family — unchanged from before the native
transport existed. Native is opt-in (final review I3: on a wire it is proven only for N-file reads and
writes and one F8 read — see Wire evidence). An explicit value always wins:
explicit Native with a PLC-5 / LogixPccc device is allowed (an operator choice) and logs one Warning per
driver instance naming the limitation. (AbCip's node-level Drivers:AbCip:DefaultTransport has no AbLegacy
counterpart — the AbLegacy default is libplctag and is not node-configurable.)
A change takes effect when the driver instance is constructed: on a deploy that changes DriverConfig
(which recreates it) and on every process start, including the restart that installs this build (see the
release note). The
AdminUI form's Transport dropdown offers "Default (libplctag)" (omits the key on save), "Native (managed,
opt-in)" and "libplctag". The driver logs transport=<effective> (default|configured) on every init, e.g.
transport=Libplctag (default). CLI: --transport default|native|libplctag (default default = libplctag for
every --plc-type). 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 PCCC transport, opt-in (final review I3)
- The default is unchanged: libplctag. A driver config with no
transportkey runsLibplctag, for every PLC family, exactly as before this release. The upgrade restart moves nothing. (An interim build on this branch defaulted SLC 500 / MicroLogix to Native; that was reverted before merge.) "transport": "Native"opts a driver into the managed PCCC-over-EtherNet/IP client (Driver.Ab.Transport). It takes effect when the driver is constructed — a deploy that changesDriverConfig, or a process start — not onReinitializeAsync. The AdminUI Transport dropdown authors it; remove the key (or pick "Default (libplctag)") to go back.- Known limits of Native (why it is opt-in):
- ⚠️ Wire evidence is narrow and simulator-only — see Wire evidence. Only
N-file reads and writes and one F8 read have touched a wire (
ab_server). The default probe addressS:0and the B, T, C, R, S, L, ST, A, O and I file types — and the T/C/R sub-element offsets (.PRE/.ACC/.LEN/.POS) — are encoded from the PCCC file-type table and are unverified natively. No real SLC, MicroLogix or PLC-5 has been exercised. Opt in per driver after checking it against the real controller. - ⚠️ Status-bit writes are refused —
T4:0.DN,T4:0.TT,T4:0.EN,C5:0.CU,C5:0.DN,R6:0.ENand the rest returnBadNotSupported; nothing is written to the PLC; the handle is kept and health is unchanged. libplctag performs a masked bit write here; a native read-modify-write of the control word would race the PLC updating TT/DN. Reads of status bits are unaffected. - PLC-5 native is N-file reads only; every other PLC-5 operation is refused with
BadNotSupported.LogixPcccframing is unverified. Explicit Native on either logs a Warning at init. - ST (string) byte order is unverified (no capture); string values may be byte-swapped.
- Unlike AbCip, the native AbLegacy runtime has no element-type check, so a mis-authored same-width
type reads as reinterpreted data (as on libplctag), not
BadTypeMismatch.
- ⚠️ Wire evidence is narrow and simulator-only — see Wire evidence. Only
N-file reads and writes and one F8 read have touched a wire (
Wire evidence (native PCCC)
Everything below is against the ab_server simulator; no real PCCC controller has been exercised.
| What | Evidence |
|---|---|
N-file read (N7:0) |
libplctag captures slc500-read-n7, micrologix-read-n7, plc5-read-n7 (codec byte parity) + native AbLegacyReadSmokeTests read on all three profiles |
N-file write (N7:5) |
capture slc500-write-n7 + native AbLegacyReadSmokeTests write/read round trip (SLC 500) |
F-file read (F8) |
one capture, slc500-read-f8 (codec parity only; no native F write on a wire) |
S (incl. the default probe S:0), B, T, C, R, L, ST, A, O, I |
none — encoded from the PCCC file-type table |
T/C/R sub-elements (.PRE/.ACC/.LEN/.POS), 6-byte T/C/R element layout |
none |
Native known gaps
- ⚠️ Simulator-only, and narrow. Only N-file reads/writes and one F8 read are on a wire (
ab_server); see Wire evidence. 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 before the
wire and reach the client as
BadNotSupported(handle kept, health unchanged; #571 closed). Native is opt-in for every family; an explicit Native on PLC-5 /logixpccclogs a Warning at init. - 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 (libplctag by default; the opt-in
native transport issues the same typed read, chunked under the PCCC payload limit).
The tag's file address (e.g. N7:0) is extended by N element count so the transport 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. The opt-in native transport also writes multi-element arrays (chunked); on libplctag 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 on libplctag (the opt-in native transport writes them), 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.