# 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](../v2/driver-specs.md). For the manual test client, see [Driver.AbCip.Cli.md](../Driver.AbCip.Cli.md). For the integration fixture coverage map, see [AbServer-Test-Fixture.md](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`** — 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`. - **Discovery** — `EnableControllerBrowse` (`@tags` walk) and `EnableDeclarationOnlyUdtGrouping` (whole-UDT read fast path). - **Alarms** — `EnableAlarmProjection` + `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](../v2/driver-specs.md). ## 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 the `ab_server` Docker fixture. See [AbServer-Test-Fixture.md](AbServer-Test-Fixture.md) for the coverage map and the `AB_SERVER_ENDPOINT` wiring. - **Manual client** — [Driver.AbCip.Cli.md](../Driver.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.