Files
lmxopcua/docs/drivers/AbCip.md
T

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 ReadAsyncReadGroupAsync / 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 — one AbCipDeviceOptions per PLC (HostAddress, PlcFamily, optional DeviceName, per-device AllowPacking / ConnectionSize overrides).
  • Tags — pre-declared AbCipTagDefinition list; Members for UDT fan-out, SafetyTag for GuardLogix safety-partition tags.
  • Probe — connectivity-probe Enabled / Interval / Timeout / ProbeTagPath.
  • DiscoveryEnableControllerBrowse (@tags walk) and EnableDeclarationOnlyUdtGrouping (whole-UDT read fast path).
  • AlarmsEnableAlarmProjection + AlarmPollInterval for 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 teststests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/ cover the driver, host-address parser, UDT planner, and alarm projection via fake tag runtimes.
  • Integration teststests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/ run against the ab_server Docker fixture. See AbServer-Test-Fixture.md for the coverage map and the AB_SERVER_ENDPOINT wiring.
  • Manual clientDriver.AbCip.Cli.md.

Operational Notes

  • Native heap is invisible to the GC. GetMemoryFootprint() reports CLR allocations only; libplctag's native Tag heap does not show up there. Watch whole-process RSS, and use ReinitializeAsync (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 EnableDeclarationOnlyUdtGrouping when every UDT's member declaration order has been hand-verified against the controller's compiled layout.