406 lines
18 KiB
Markdown
406 lines
18 KiB
Markdown
# AB CIP — Performance knobs
|
||
|
||
Phase 3 of the AB CIP driver plan introduces a small set of operator-tunable
|
||
performance knobs that change how the driver talks to the controller without
|
||
altering the address space or per-tag semantics. They consolidate decisions
|
||
that Kepware exposes as a slider / advanced page so deployments running into
|
||
high-latency PLCs, narrow-CPU CompactLogix parts, or legacy ControlLogix
|
||
firmware have an explicit lever to pull.
|
||
|
||
This document is the home for those knobs as PRs land. PR abcip-3.1 ships the
|
||
first knob: per-device **CIP Connection Size**.
|
||
|
||
## Connection Size
|
||
|
||
### What it is
|
||
|
||
CIP Connection Size — the byte ceiling on a single Forward Open response
|
||
fragment, set during the EtherNet/IP Forward Open handshake. Larger
|
||
connection sizes pack more tags into a single CIP RTT (higher request-packing
|
||
density, fewer round-trips for the same scan list); smaller connection sizes
|
||
stay compatible with legacy or narrow-buffer firmware that rejects oversized
|
||
Forward Open requests.
|
||
|
||
### Family defaults
|
||
|
||
The driver picks a Connection Size from the per-family profile when the
|
||
device-level override is unset:
|
||
|
||
| Family | Default | Rationale |
|
||
|---|---:|---|
|
||
| `ControlLogix` | `4002` | Large Forward Open — FW20+ |
|
||
| `GuardLogix` | `4002` | Same wire protocol as ControlLogix |
|
||
| `CompactLogix` | `504` | 5069-L1/L2/L3 narrow-buffer parts (5370 family) |
|
||
| `Micro800` | `488` | Hard cap on Micro800 firmware |
|
||
|
||
These map straight to libplctag's `connection_size` attribute and match the
|
||
defaults Kepware uses out of the box for the same families.
|
||
|
||
### Override knob
|
||
|
||
`AbCipDeviceOptions.ConnectionSize` (`int?`, default `null`) overrides the
|
||
family default for one device. Bind it through driver config JSON:
|
||
|
||
```json
|
||
{
|
||
"Devices": [
|
||
{
|
||
"HostAddress": "ab://10.0.0.5/1,0",
|
||
"PlcFamily": "ControlLogix",
|
||
"ConnectionSize": 504
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
The override threads through every libplctag handle the driver creates for
|
||
that device — read tags, write tags, probe tags, UDT-template reads, the
|
||
`@tags` walker, and BOOL-in-DINT parent runtimes. There is no per-tag
|
||
override; one Connection Size applies to the whole controller (matches CIP
|
||
session semantics).
|
||
|
||
### Valid range
|
||
|
||
`[500..4002]` bytes. This matches the slider Kepware exposes for the same
|
||
family. Values outside the range fail driver `InitializeAsync` with an
|
||
`InvalidOperationException` — there's no silent clamp; misconfigured devices
|
||
fail loudly so operators see the problem at deploy time.
|
||
|
||
| Value | Behaviour |
|
||
|---|---|
|
||
| `null` | Use family default (4002 / 504 / 488) |
|
||
| `499` or below | Driver init fault — out-of-range |
|
||
| `500..4002` | Threaded through to libplctag |
|
||
| `4003` or above | Driver init fault — out-of-range |
|
||
|
||
### Legacy-firmware caveat
|
||
|
||
ControlLogix firmware **v19 and earlier** caps the CIP buffer at **504
|
||
bytes** — Connection Sizes above that cause the controller to reject the
|
||
Forward Open with CIP error 0x01/0x113. The 5069-L1/L2/L3 CompactLogix narrow
|
||
parts are subject to the same cap.
|
||
|
||
The driver emits a warning via `AbCipDriverOptions.OnWarning` when the
|
||
configured Connection Size **exceeds 511** *and* the device's family profile
|
||
default is also at-or-below the legacy cap (i.e. CompactLogix with default
|
||
504, or Micro800 with default 488). Production hosting should wire
|
||
`OnWarning` to the application logger; the unit tests (`AbCipConnectionSizeTests`)
|
||
collect into a list to assert which warnings fired.
|
||
|
||
The warning fires once per device at `InitializeAsync`. It does not block
|
||
initialisation — operators may need the override anyway when running newer
|
||
CompactLogix firmware that does support the larger Forward Open. The
|
||
controller will reject the connection at runtime if it can't honour the size,
|
||
and that surfaces through the standard `IHostConnectivityProbe` channel.
|
||
|
||
### Performance trade-off
|
||
|
||
| Larger Connection Size | Smaller Connection Size |
|
||
|---|---|
|
||
| More tags per CIP RTT — higher throughput | Compatible with legacy / narrow firmware |
|
||
| Bigger buffers held by libplctag native (RSS impact) | Lower memory footprint |
|
||
| Forward Open rejected on FW19- ControlLogix | Always works (assuming ≥500) |
|
||
| Required for high-density scan lists | Forces more round-trips — higher latency |
|
||
|
||
For most FW20+ ControlLogix shops, the default `4002` is correct and the
|
||
override is unnecessary. The override is mainly useful when:
|
||
|
||
1. **Migrating off Kepware** with a controller-specific slider value already
|
||
tuned in production — set Connection Size to match.
|
||
2. **Mixed-firmware fleets** where some controllers are still on FW19 — set
|
||
the legacy controllers explicitly to `504`.
|
||
3. **CompactLogix L1/L2/L3** running newer firmware that supports a larger
|
||
Forward Open than the family-default 504 — bump the override up.
|
||
4. **Micro800** never goes above `488`; the override is for documentation /
|
||
discoverability rather than capability change.
|
||
|
||
### libplctag wrapper limitation
|
||
|
||
The libplctag .NET wrapper (1.5.x) does not expose `connection_size` as a
|
||
public `Tag` property. The driver propagates the value via reflection on the
|
||
wrapper's internal `NativeTagWrapper.SetIntAttribute("connection_size", N)`
|
||
after `InitializeAsync` — equivalent to libplctag's
|
||
`plc_tag_set_int_attribute`. Because libplctag native parses
|
||
`connection_size` only at create time, this is **best-effort** until either:
|
||
|
||
- the libplctag .NET wrapper exposes `ConnectionSize` directly (planned in
|
||
the upstream backlog), in which case the reflection no-ops cleanly, or
|
||
- libplctag native gains post-create hot-update for `connection_size`, in
|
||
which case the call lands as intended.
|
||
|
||
In the meantime the value is correctly stored on `DeviceState.ConnectionSize`
|
||
+ surfaces in every `AbCipTagCreateParams` the driver builds, so the override
|
||
is observable end-to-end through the public driver surface and unit tests
|
||
even if the underlying wrapper isn't yet honouring it on the wire.
|
||
|
||
Operators who need *guaranteed* Connection Size enforcement against FW19
|
||
controllers today can pin `libplctag` to a wrapper version that exposes
|
||
`ConnectionSize` once one is available, or run a libplctag native build
|
||
patched for runtime updates. Both paths are tracked in the AB CIP plan.
|
||
|
||
### See also
|
||
|
||
- [`docs/Driver.AbCip.Cli.md`](../Driver.AbCip.Cli.md) — AB CIP CLI uses the
|
||
family default ConnectionSize on each invocation; per-device overrides only
|
||
apply through the driver's device-config JSON, not the CLI's command-line.
|
||
- [`docs/drivers/AbServer-Test-Fixture.md`](AbServer-Test-Fixture.md) §5 —
|
||
ab_server simulator does not enforce the narrow CompactLogix cap, so
|
||
Connection Size correctness is verified by unit tests + Emulate-rig live
|
||
smokes only.
|
||
- [`PlcFamilies/AbCipPlcFamilyProfile.cs`](../../src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/PlcFamilies/AbCipPlcFamilyProfile.cs) —
|
||
per-family default values.
|
||
- [`AbCipConnectionSize`](../../src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipConnectionSize.cs) —
|
||
range bounds + legacy-firmware threshold constants.
|
||
|
||
## Addressing mode
|
||
|
||
### What it is
|
||
|
||
CIP exposes two equivalent ways to address a Logix tag on the wire:
|
||
|
||
1. **Symbolic** — the request carries the tag's ASCII name and the controller
|
||
parses + resolves the path on every read. This is the libplctag default
|
||
and what every previous driver build has used.
|
||
2. **Logical** — the request carries a CIP Symbol Object instance ID (a small
|
||
integer assigned by the controller when the project was downloaded). The
|
||
controller skips ASCII parsing entirely; the lookup is a single
|
||
instance-table dereference.
|
||
|
||
Logical addressing is faster on the controller side and produces smaller
|
||
request frames. The trade-off is that the driver has to learn the
|
||
name → instance-id mapping once, by reading the `@tags` pseudo-tag at
|
||
startup, and the resolution step has to repeat after a controller program
|
||
download (instance IDs are re-assigned).
|
||
|
||
### Enum values
|
||
|
||
`AbCipDeviceOptions.AddressingMode` (`AddressingMode` enum, default
|
||
`Auto`) takes one of three values:
|
||
|
||
| Value | Behaviour |
|
||
|---|---|
|
||
| `Auto` | Driver picks. **Currently resolves to `Symbolic`** — a future PR will plumb a real auto-detection heuristic (firmware version + symbol-table size). |
|
||
| `Symbolic` | Force ASCII symbolic addressing on the wire. The historical default. |
|
||
| `Logical` | Use CIP logical-segment / instance-ID addressing. Triggers a one-time `@tags` walk at the first read; subsequent reads consult the cached map. |
|
||
|
||
`Auto` is documented as "Symbolic-for-now" so deployments setting `Auto`
|
||
explicitly today will silently flip to a real heuristic when one ships,
|
||
matching the spirit of the toggle. Operators who want to pin the wire
|
||
behaviour should set `Symbolic` or `Logical` directly.
|
||
|
||
### Family compatibility
|
||
|
||
Logical addressing depends on the controller implementing CIP Symbol Object
|
||
class 0x6B with stable instance IDs. Older AB families don't:
|
||
|
||
| Family | Logical addressing supported? | Why |
|
||
|---|---|---|
|
||
| `ControlLogix` | yes | Native class 0x6B support, FW10+ |
|
||
| `CompactLogix` | yes | Same wire protocol as ControlLogix |
|
||
| `GuardLogix` | yes | Same wire protocol; safety partition is tag-level, not addressing-level |
|
||
| `Micro800` | **no** | Firmware does not implement class 0x6B; instance-ID reads trip CIP "Path Segment Error" 0x04 |
|
||
| `SLC500` / `PLC5` | **no** | Pre-CIP families; PCCC bridging only — no Symbol Object at all |
|
||
|
||
When `AddressingMode = Logical` is set on an unsupported family, the driver
|
||
**falls back to Symbolic with a warning** (via `OnWarning`) instead of
|
||
faulting. This keeps mixed-firmware deployments working — operators can ship
|
||
a uniform "Logical" config across the fleet and let the driver downgrade
|
||
the families that can't honour it.
|
||
|
||
The driver-level decision is exposed via
|
||
`PlcFamilies.AbCipPlcFamilyProfile.SupportsLogicalAddressing` and resolved at
|
||
`AbCipDriver.InitializeAsync` time; the resolved mode is stored on
|
||
`DeviceState.AddressingMode` and threaded through every
|
||
`AbCipTagCreateParams` from then on.
|
||
|
||
### One-time symbol-table walk
|
||
|
||
The first read on a Logical-mode device triggers a one-time `@tags` walk via
|
||
`LibplctagTagEnumerator` (the same component used for opt-in controller
|
||
browse). The driver caches the resulting name → instance-id map on
|
||
`DeviceState.LogicalInstanceMap`; subsequent reads consult the cache without
|
||
issuing another walk. The walk is gated by a per-device `SemaphoreSlim` so
|
||
parallel first-reads serialise on a single dispatch.
|
||
|
||
The walk happens in `AbCipDriver.EnsureLogicalMappingsAsync` and runs only
|
||
for devices that have actually resolved to `Logical`. Symbolic-mode devices
|
||
skip the walk entirely. Walk failures are non-fatal: the
|
||
`LogicalWalkComplete` flag still flips to `true` so the driver does not
|
||
re-attempt indefinitely, and per-tag handles fall back to Symbolic addressing
|
||
on the wire (libplctag's default).
|
||
|
||
A controller program download invalidates the instance IDs. There is no
|
||
auto-invalidation today — operators trigger a fresh walk by either
|
||
restarting the driver or calling `RebrowseAsync` (the same surface that
|
||
clears the UDT template cache) with logic-mode plumbing extended in a
|
||
future PR. For now, restart-on-download is the recommended workflow.
|
||
|
||
### libplctag wrapper limitation
|
||
|
||
The libplctag .NET wrapper (1.5.x) does **not** expose a public knob for
|
||
instance-ID addressing. The driver translates Logical-mode params into
|
||
libplctag attributes via reflection on
|
||
`NativeTagWrapper.SetAttributeString("use_connected_msg", "1")` +
|
||
`SetAttributeString("cip_addr", "0x6B,N")` — same best-effort fallback
|
||
pattern as the Connection Size knob.
|
||
|
||
This means **Logical mode is observable end-to-end through the public
|
||
driver surface and unit tests today**, but the actual wire behaviour
|
||
remains Symbolic until either:
|
||
|
||
- the upstream libplctag .NET wrapper exposes the
|
||
`UseConnectedMessaging` + `CipAddr` properties on `Tag` directly
|
||
(planned in the upstream backlog), in which case the reflection no-ops
|
||
cleanly, or
|
||
- libplctag native gains post-create hot-update for `cip_addr`, in which
|
||
case the call lands as intended.
|
||
|
||
The driver-level bookkeeping (resolved mode, instance-id map, family
|
||
compatibility, fall-back warning) is fully wired so the upgrade path is
|
||
purely a wrapper-version bump.
|
||
|
||
### Performance trade-off
|
||
|
||
| Symbolic addressing | Logical addressing |
|
||
|---|---|
|
||
| Works everywhere | Requires Symbol Object class 0x6B |
|
||
| ASCII parse on every read (controller-side cost) | One-time walk; instance-id lookup thereafter |
|
||
| No first-read latency | First read on a device pays the `@tags` walk |
|
||
| Smaller code surface | Stale on program download — restart driver to re-walk |
|
||
| Best for small / sparse tag sets | Best for >500-tag scans with stable controller |
|
||
|
||
For scan lists in the **single-digit-tag** range, the per-poll ASCII parse
|
||
cost is invisible. For **medium** scan lists (~100 tags) the gain is real
|
||
but small — typically 5–10% per CIP RTT depending on tag-name length. The
|
||
break-even point is where the ASCII-parse overhead starts dominating,
|
||
roughly **>500 tags** in a tight scan loop, which is also where libplctag's
|
||
own request-packing benefits compound. Large MES / batch projects with
|
||
many UDT instances are the canonical case.
|
||
|
||
### Driver config JSON
|
||
|
||
Bind the toggle through the driver-config JSON:
|
||
|
||
```json
|
||
{
|
||
"Devices": [
|
||
{
|
||
"HostAddress": "ab://10.0.0.5/1,0",
|
||
"PlcFamily": "ControlLogix",
|
||
"AddressingMode": "Logical"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
`"Auto"`, `"Symbolic"`, and `"Logical"` parse case-insensitively. Omitting
|
||
the field defaults to `"Auto"`.
|
||
|
||
### See also
|
||
|
||
- [`AbCipDriverOptions.AddressingMode`](../../src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverOptions.cs) —
|
||
enum definition + per-value docstrings.
|
||
- [`AbCipPlcFamilyProfile.SupportsLogicalAddressing`](../../src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/PlcFamilies/AbCipPlcFamilyProfile.cs) —
|
||
family compatibility table source-of-truth.
|
||
- [`docs/drivers/AbServer-Test-Fixture.md`](AbServer-Test-Fixture.md) §
|
||
"What it actually covers" — Logical-mode fixture coverage status.
|
||
- [`AbCipAddressingModeBenchTests`](../../tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbCipAddressingModeBenchTests.cs) —
|
||
scaffold for the wall-clock comparison; gated on `[AbServerFact]`.
|
||
|
||
## Read strategy (PR abcip-3.3)
|
||
|
||
A per-device toggle that controls how multi-member UDT batches are read.
|
||
The default `Auto` value matches every previous build's behaviour for dense
|
||
reads but switches to per-member bundling when only a handful of members of
|
||
a large UDT are subscribed — the canonical "5 of 50" sparse-subscription
|
||
case where reading the whole UDT buffer just to extract a few fields wastes
|
||
wire bandwidth.
|
||
|
||
### Three modes
|
||
|
||
| Mode | When to use |
|
||
|---|---|
|
||
| `WholeUdt` | Most members of every subscribed UDT are read together. One libplctag read per parent UDT, members decoded in-memory at their byte offsets. The task #194 default. |
|
||
| `MultiPacket` | A few members of a large UDT are subscribed at a time. One read per subscribed member, bundled per parent into one CIP Multi-Service Packet. |
|
||
| `Auto` (default) | Planner picks per-batch from the subscribed-member fraction (see *Sparsity threshold*). |
|
||
|
||
### Sparsity threshold
|
||
|
||
Auto mode divides `subscribedMembers / totalMembers` for each parent UDT and
|
||
picks `MultiPacket` when the fraction is **strictly less than** the
|
||
threshold, else `WholeUdt`. Default threshold `0.25` — a 1/4 subscription is
|
||
the rough break-even where the wire-cost of one whole-UDT read still beats
|
||
N member reads on a ControlLogix 4002-byte connection-size buffer; above
|
||
1/4, the per-member overhead dominates.
|
||
|
||
Tune via `AbCipDeviceOptions.MultiPacketSparsityThreshold` (clamped to
|
||
`[0..1]`). Threshold `0.0` = "never MultiPacket"; `1.0` = "always MultiPacket
|
||
when any member is subscribed."
|
||
|
||
### Family compatibility
|
||
|
||
`MultiPacket` requires CIP service `0x0A` (Multi-Service Packet) on the
|
||
controller. Source of truth is
|
||
[`AbCipPlcFamilyProfile.SupportsRequestPacking`](../../src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/PlcFamilies/AbCipPlcFamilyProfile.cs):
|
||
|
||
| Family | `SupportsRequestPacking` |
|
||
|---|---|
|
||
| ControlLogix | yes |
|
||
| CompactLogix | yes |
|
||
| GuardLogix | yes (wire identical to ControlLogix) |
|
||
| Micro800 | **no** |
|
||
| SLC500 / PLC5 (when those profiles ship) | **no** |
|
||
|
||
User-forced `MultiPacket` against a non-packing family logs a warning at
|
||
device init and falls back to `WholeUdt`. `Auto` against a non-packing
|
||
family stays `Auto` at the device level — the per-batch heuristic caps the
|
||
strategy to `WholeUdt` so the wire never sees a Multi-Service-Packet against
|
||
a controller that can't decode it.
|
||
|
||
### libplctag wrapper limitation
|
||
|
||
The libplctag .NET wrapper (1.5.x) does not expose the `0x0A` service as a
|
||
public knob — same wrapper-version constraint that gates PR abcip-3.1's
|
||
`connection_size` and PR abcip-3.2's instance-ID addressing. Today's
|
||
MultiPacket runtime therefore issues N libplctag reads sequentially when
|
||
the planner picks the strategy; the wire-level bundling lands cleanly when
|
||
an upstream wrapper release exposes the primitive.
|
||
|
||
The driver-level bookkeeping (resolved strategy, per-batch heuristic,
|
||
family-compat fall-back, per-device dispatch counters) is fully wired so
|
||
the upgrade path is a wrapper-version bump only — the planner already
|
||
produces the right plan, and `AbCipMultiPacketReadPlanner.Build` is
|
||
covered by unit tests that pin the plan shape rather than wire bytes.
|
||
|
||
### Driver config JSON
|
||
|
||
```json
|
||
{
|
||
"Devices": [
|
||
{
|
||
"HostAddress": "ab://10.0.0.5/1,0",
|
||
"PlcFamily": "ControlLogix",
|
||
"ReadStrategy": "Auto",
|
||
"MultiPacketSparsityThreshold": 0.25
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
`"Auto"`, `"WholeUdt"`, and `"MultiPacket"` parse case-insensitively.
|
||
Omitting the field defaults to `"Auto"`. Omitting
|
||
`MultiPacketSparsityThreshold` defaults to `0.25`.
|
||
|
||
### See also
|
||
|
||
- [`AbCipDriverOptions.ReadStrategy`](../../src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverOptions.cs) —
|
||
enum definition + per-value docstrings.
|
||
- [`AbCipMultiPacketReadPlanner`](../../src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipMultiPacketReadPlanner.cs) —
|
||
plan shape + Auto-mode heuristic.
|
||
- [`AbCipPlcFamilyProfile.SupportsRequestPacking`](../../src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/PlcFamilies/AbCipPlcFamilyProfile.cs) —
|
||
family compatibility table source-of-truth.
|
||
- [`AbCipReadStrategyTests`](../../tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipReadStrategyTests.cs) —
|
||
device-init resolution, heuristic edges, dispatch counters, DTO round-trip.
|
||
- [`AbCipEmulateMultiPacketReadTests`](../../tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/AbCipEmulateMultiPacketReadTests.cs) —
|
||
golden-box-tier wire-level coverage scaffold; gated on `AB_SERVER_PROFILE=emulate`.
|