105 lines
7.2 KiB
Markdown
105 lines
7.2 KiB
Markdown
# 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.
|