7.2 KiB
AB CIP Driver
In-process native-protocol driver that exposes Allen-Bradley CIP / EtherNet-IP
controllers as OPC UA nodes. It runs inside the OtOpcUa server's .NET 10 AnyCPU
process and talks to the PLC through the libplctag.NET wrapper — no gateway, no
sidecar. One driver instance can serve many devices; per-device routing is keyed
on the canonical ab://gateway[:port]/cip-path host-address string.
Supported families: ControlLogix, CompactLogix, Micro800, and
GuardLogix. CIP has no native push model, so subscriptions are a polling
overlay on top of IReadable.
For the driver spec (capability surface, config shape, type mapping), see docs/v2/driver-specs.md §3. For the manual test client, see Driver.AbCip.Cli.md. For the integration fixture coverage map, see AbServer-Test-Fixture.md.
Project Layout
| Project | Role |
|---|---|
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/ |
The driver — AbCipDriver, the libplctag runtime/enumerator/template-reader wrappers, the UDT read planner + template decoders, the host-address parser, and the ALMD alarm projection. |
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/ |
AbCipDriverOptions, AbCipDeviceOptions, AbCipTagDefinition / AbCipStructureMember, and the AbCipDataType / AbCipPlcFamily enums bound from the driver's DriverConfig JSON. |
Per family the AbCipPlcFamilyProfile (PlcFamilies/AbCipPlcFamilyProfile.cs)
supplies the libplctag plc attribute, default CIP path, ConnectionSize, and
request-packing / connected-messaging quirks — ControlLogix is the baseline and
each other family is a delta (Micro800 is unconnected-only with no backplane
routing; GuardLogix shares the ControlLogix wire protocol with a tag-level safety
partition).
Capability Surface
AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IAlarmSource, IDisposable, IAsyncDisposable
(Driver.AbCip/AbCipDriver.cs). It adds IAlarmSource over the Modbus /
AB Legacy surface.
| Capability | Implementation entry point | Notes |
|---|---|---|
ITagDiscovery |
DiscoverAsync |
Emits pre-declared tags under per-device folders; UDT tags with declared Members fan out into a sub-folder + one variable per member. With EnableControllerBrowse the @tags symbol table is walked into a Discovered/ folder (system/module/routine tags filtered out). |
IReadable |
ReadAsync → ReadGroupAsync / ReadSingleAsync |
Per-tag reads; opt-in whole-UDT grouping (EnableDeclarationOnlyUdtGrouping) collapses N member reads into one. |
IWritable |
WriteAsync |
BOOL-within-DINT writes do a per-parent read-modify-write under a lock; SafetyTag and non-writable tags return BadNotWritable. |
ISubscribable |
SubscribeAsync driven by the shared PollGroupEngine |
CIP has no push model — subscriptions become polling groups. |
IHostConnectivityProbe |
ProbeLoopAsync + GetHostStatuses |
One probe loop per device reading Probe.ProbeTagPath; no path configured ⇒ a warning is logged and the device stays Unknown. |
IPerCallHostResolver |
ResolveHost |
Routes each call to the tag's DeviceHostAddress, the breaker key for the resilience pipeline so one dead PLC trips only its own breaker. |
IAlarmSource |
AbCipAlarmProjection (ALMD) |
Opt-in via EnableAlarmProjection; off by default the subscribe path is a no-op so capability negotiation still works. |
Addressing Model
Per-device host addresses are the canonical ab://gateway[:port]/cip-path form
parsed by AbCipHostAddress.TryParse (AbCipHostAddress.cs). The parsed
CipPath is handed to libplctag verbatim, so no wire-layer translation is
needed:
| Form | Meaning |
|---|---|
ab://10.0.0.5/1,0 |
Single-chassis ControlLogix, CPU in slot 0 |
ab://10.0.0.5/1,2,2,192.168.50.20,1,0 |
Bridged ControlLogix (routed path) |
ab://10.0.0.5/ |
Micro800 / no-backplane device (empty path) |
ab://10.0.0.5:44818/1,0 |
Explicit EIP port (default 44818) |
Tags carry a Logix symbolic TagPath (controller or program scope). UDT-typed
tags are declared as AbCipDataType.Structure with a Members list; discovery
fans each member out as {tag.Name}.{member.Name}, and the read planner can
collapse a batch of members into one whole-UDT read when
EnableDeclarationOnlyUdtGrouping is set. The whole-UDT fast path is opt-in
because Studio 5000 may reorder members vs declaration order; decoding at
declaration-order offsets against a reordered layout yields silently-plausible
wrong numbers.
Configuration
AbCipDriverOptions (Driver.AbCip.Contracts/AbCipDriverOptions.cs) binds from
the driver's DriverConfig JSON. Key fields:
Devices— oneAbCipDeviceOptionsper PLC (HostAddress,PlcFamily, optionalDeviceName, per-deviceAllowPacking/ConnectionSizeoverrides).Tags— pre-declaredAbCipTagDefinitionlist;Membersfor UDT fan-out,SafetyTagfor GuardLogix safety-partition tags.Probe— connectivity-probeEnabled/Interval/Timeout/ProbeTagPath.- Discovery —
EnableControllerBrowse(@tagswalk) andEnableDeclarationOnlyUdtGrouping(whole-UDT read fast path). - Alarms —
EnableAlarmProjection+AlarmPollIntervalfor the ALMD projection.
Full per-field descriptions live in AbCipDriverOptions.cs. The JSON skeleton is
reproduced in docs/v2/driver-specs.md §3.
Alarm Projection
IAlarmSource is served by AbCipAlarmProjection, which polls each subscribed
ALMD UDT's InFaulted + Severity members at AlarmPollInterval and fires
OnAlarmEvent on raise/clear transitions. It is ALMD-only in this pass (ALMA
analog alarms are a follow-up) and disabled by default — shops running FT
Alarm & Events should keep it off and take alarms through the native route, since
the projection semantics don't exactly mirror Rockwell FT A&E.
Testing
- Unit tests —
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/cover the driver, host-address parser, UDT planner, and alarm projection via fake tag runtimes. - Integration tests —
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/run against theab_serverDocker fixture. See AbServer-Test-Fixture.md for the coverage map and theAB_SERVER_ENDPOINTwiring. - Manual client — Driver.AbCip.Cli.md.
Operational Notes
- Native heap is invisible to the GC.
GetMemoryFootprint()reports CLR allocations only; libplctag's nativeTagheap does not show up there. Watch whole-process RSS, and useReinitializeAsync(tears down + re-creates every device's libplctag handles) as the remediation for native-heap growth. - Handle eviction on failure — a non-zero libplctag status or a transport exception evicts the cached tag runtime so the next read/write re-creates a fresh handle, mirroring the probe loop's recreate-on-failure behaviour.
- Declaration-only UDT grouping is a footgun unless verified — only enable
EnableDeclarationOnlyUdtGroupingwhen every UDT's member declaration order has been hand-verified against the controller's compiled layout.