152
docs/drivers/AbCip-Operability.md
Normal file
152
docs/drivers/AbCip-Operability.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# AB CIP — Operability knobs
|
||||
|
||||
Phase 4 of the AB CIP driver plan introduces operator-tunable behaviour that
|
||||
changes how the driver schedules per-tag traffic, deduplicates updates, and
|
||||
surfaces health — knobs that an operator typically reaches for *after* the
|
||||
address space is in place and the deployment is past the green-field phase.
|
||||
The Phase 3 doc (`AbCip-Performance.md`) covers connection-shape and
|
||||
read-strategy knobs; this doc is the home for the per-tag scheduling and
|
||||
operability levers as PRs land.
|
||||
|
||||
PR abcip-4.1 ships the first knob: per-tag **Scan Rate** (Kepware-parity scan
|
||||
classes).
|
||||
|
||||
## Per-tag scan rate
|
||||
|
||||
### What it is
|
||||
|
||||
A per-tag override of the OPC UA subscription's `publishingInterval`. The AB
|
||||
CIP driver mirrors the Galaxy hierarchy as a single OPC UA address space, so
|
||||
every tag served from one driver normally ticks at the publishing interval the
|
||||
client requested when it created the Subscription. This knob lets specific
|
||||
tags publish at a different cadence — fast HMI tags at 100 ms, batch /
|
||||
historian tags at 1–10 s — without forcing the operator to split tags into
|
||||
separate subscriptions or driver instances.
|
||||
|
||||
It is the Kepware "scan classes" model expressed per-tag. The same shape is
|
||||
already shipped in the S7 driver (`S7TagDefinition.ScanGroup`) and the AB
|
||||
Legacy / TwinCAT drivers; AB CIP adopts a leaner per-tag-only form because the
|
||||
CIP single-connection model means the practical knob a deployment reaches for
|
||||
is "this one tag, faster", not "every tag in this group".
|
||||
|
||||
### How it interacts with OPC UA publishingInterval
|
||||
|
||||
OPC UA semantics:
|
||||
|
||||
- The Subscription's `publishingInterval` is the *upper bound* on how often
|
||||
the server publishes a NotificationMessage. Each MonitoredItem also has its
|
||||
own `samplingInterval`; that's where this knob lands.
|
||||
- A per-tag `samplingInterval` shorter than the Subscription's
|
||||
`publishingInterval` means the server samples faster but only publishes at
|
||||
the next Subscription tick — clients may receive multiple values for one
|
||||
tag in a single Publish response.
|
||||
- A per-tag `samplingInterval` longer than the Subscription's
|
||||
`publishingInterval` is legal too — the server simply skips ticks for that
|
||||
tag.
|
||||
|
||||
AB CIP-side: the driver's `SubscribeAsync` receives one `publishingInterval`
|
||||
plus a list of tag references. With per-tag `ScanRateMs` it buckets the input
|
||||
list by resolved interval and registers one `PollGroupEngine` subscription per
|
||||
bucket. Each bucket runs an independent timer, so a 100 ms tag never waits
|
||||
for a 1000 ms tag's `Task.Delay` to expire.
|
||||
|
||||
### Override knob
|
||||
|
||||
`AbCipTagDefinition.ScanRateMs` (`int?`, default `null`). `null` = use the
|
||||
subscription's default `publishingInterval` (legacy behaviour). Bind via
|
||||
driver config JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"Tags": [
|
||||
{
|
||||
"Name": "Motor1.Speed",
|
||||
"DeviceHostAddress": "ab://10.0.0.5/1,0",
|
||||
"TagPath": "Motor1.Speed",
|
||||
"DataType": "DInt",
|
||||
"ScanRateMs": 100
|
||||
},
|
||||
{
|
||||
"Name": "Motor1.RunHours",
|
||||
"DeviceHostAddress": "ab://10.0.0.5/1,0",
|
||||
"TagPath": "Motor1.RunHours",
|
||||
"DataType": "DInt",
|
||||
"ScanRateMs": 5000
|
||||
},
|
||||
{
|
||||
"Name": "Motor1.NamePlate",
|
||||
"DeviceHostAddress": "ab://10.0.0.5/1,0",
|
||||
"TagPath": "Motor1.NamePlate",
|
||||
"DataType": "String"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Result: three buckets — 100 ms, 5000 ms, and the subscription default for
|
||||
`NamePlate`. UDT members inherit the parent tag's `ScanRateMs` at fan-out
|
||||
time, so a UDT declared at 100 ms publishes every member at 100 ms without
|
||||
the operator having to repeat the override on each member.
|
||||
|
||||
### Floor and degenerate cases
|
||||
|
||||
- `PollGroupEngine` floors every bucket at **100 ms** — a `ScanRateMs: 25`
|
||||
is clamped up. The floor matches the Modbus / S7 / TwinCAT floors and
|
||||
protects the wire from sub-mailbox-scan polling.
|
||||
- `ScanRateMs: 0` and negative values are treated as unset — the tag falls
|
||||
back to the subscription default. Mis-typed config degrades, doesn't fault.
|
||||
- A `ScanRateMs` equal to the subscription default collapses into the same
|
||||
bucket as plain tags. The driver doesn't fragment poll loops when the
|
||||
override is redundant.
|
||||
- Tags whose names don't appear in the driver's tag map (typo / discovery
|
||||
miss) fall through to the subscription default — same "config typo
|
||||
degrades" stance as the rest of the driver.
|
||||
|
||||
### Wire impact
|
||||
|
||||
Per-bucket independent timers do **not** parallelise CIP traffic. The driver
|
||||
serializes wire-side reads through its per-device libplctag handles, so a
|
||||
fast bucket and a slow bucket trade off against each other on the wire — the
|
||||
multi-rate split decouples *cadence* (the 100 ms bucket isn't queued behind
|
||||
the 1000 ms bucket's `Task.Delay`), not *throughput*. The wire still moves
|
||||
one CIP request at a time per device.
|
||||
|
||||
If you're reading a large tag set and the slow bucket starves the fast
|
||||
bucket, the lever is `AbCipDeviceOptions.ConnectionSize` (Phase 3) — pack
|
||||
more tags into one CIP RTT so the slow bucket finishes faster. Per-tag scan
|
||||
rate is a *scheduling* knob, not a *throughput* knob.
|
||||
|
||||
### Comparison to Kepware scan classes
|
||||
|
||||
| Kepware concept | AB CIP equivalent |
|
||||
|---|---|
|
||||
| Scan class table (named groups → rate) | implicit: each distinct `ScanRateMs` value is its own bucket |
|
||||
| Default scan class | OPC UA Subscription's `publishingInterval` |
|
||||
| Per-tag scan class assignment | `AbCipTagDefinition.ScanRateMs` |
|
||||
| "Scan mode: Respect client" | always — the OPC UA `publishingInterval` is the default |
|
||||
| "Force write" / "Write through cache" | not exposed — AB CIP writes always go to the wire |
|
||||
|
||||
The leaner shape (per-tag rate, not named groups) keeps the JSON config flat
|
||||
and reflects how operators tend to use the knob in practice — a handful of
|
||||
"this specific tag needs to be fast" overrides on top of a sensible default,
|
||||
rather than a separate tier of scan-class definitions.
|
||||
|
||||
### Verification
|
||||
|
||||
- **Unit**: `AbCipPerTagScanRateTests` (`tests/.../AbCip.Tests`). Asserts
|
||||
bucketing math, default-rate collapse, UDT member inheritance, JSON DTO
|
||||
round-trip, and end-to-end cadence against the in-process fake.
|
||||
- **Integration**: `AbCipPerTagScanRateTests`
|
||||
(`tests/.../AbCip.IntegrationTests`). Drives two tags at 100 ms / 1000 ms
|
||||
against a live `ab_server` and asserts the bucket count + each tag receives
|
||||
the initial-data push.
|
||||
- **E2E**: `scripts/e2e/test-abcip.ps1` — see the *PerTagScanRate* assertion.
|
||||
|
||||
### Cross-references
|
||||
|
||||
- `docs/Driver.AbCip.Cli.md` — there is no CLI surface change for this knob;
|
||||
scan rate is a config-time concern.
|
||||
- `docs/drivers/AbCip-Performance.md` — Phase 3 throughput knobs that pair
|
||||
with per-tag scan rate when a slow bucket starves a fast one.
|
||||
- S7 driver `ScanGroup` model in `src/.../S7DriverOptions.cs` — the
|
||||
named-group form of the same idea.
|
||||
Reference in New Issue
Block a user