Merge pull request '[abcip] AbCip — Configurable CIP Connection Size per device' (#367) from auto/abcip/3.1 into auto/driver-gaps
This commit was merged in pull request #367.
This commit is contained in:
@@ -81,3 +81,25 @@ otopcua-abcip-cli subscribe -g ab://10.0.0.5/1,0 -t Motor01_Speed --type Real -i
|
|||||||
- **"Is this GuardLogix safety tag writable from non-safety?"** → `write` and
|
- **"Is this GuardLogix safety tag writable from non-safety?"** → `write` and
|
||||||
read the status code — safety tags surface `BadNotWritable` / CIP errors,
|
read the status code — safety tags surface `BadNotWritable` / CIP errors,
|
||||||
non-safety tags surface `Good`.
|
non-safety tags surface `Good`.
|
||||||
|
|
||||||
|
## Connection Size
|
||||||
|
|
||||||
|
PR abcip-3.1 introduced a per-device `ConnectionSize` override on the driver
|
||||||
|
side (`AbCipDeviceOptions.ConnectionSize`, range `500..4002`). The CLI does
|
||||||
|
not expose a flag for it — every CLI invocation uses the family-default
|
||||||
|
Connection Size (4002 / 504 / 488 depending on `--family`). When a Forward
|
||||||
|
Open is rejected with a CIP error like `0x01/0x113` ("connection request
|
||||||
|
size invalid"), the symptom is almost always a **mismatch between the chosen
|
||||||
|
family default and the controller firmware**:
|
||||||
|
|
||||||
|
- **v19-and-earlier ControlLogix** caps at 504 — pick `--family CompactLogix`
|
||||||
|
on the CLI to fall back to that narrower default.
|
||||||
|
- **5069-L1/L2/L3 CompactLogix** narrow-buffer parts also cap at 504, which
|
||||||
|
is the family default already.
|
||||||
|
- **FW20+ ControlLogix** accepts the full 4002.
|
||||||
|
|
||||||
|
For the warning *"AbCip device 'X' family 'Y' uses a narrow-buffer profile
|
||||||
|
(default ConnectionSize Z); the configured ConnectionSize N exceeds the
|
||||||
|
511-byte legacy-firmware cap..."* see
|
||||||
|
[`docs/drivers/AbCip-Performance.md`](drivers/AbCip-Performance.md) — that
|
||||||
|
warning is fired by the driver host, not the CLI.
|
||||||
|
|||||||
153
docs/drivers/AbCip-Performance.md
Normal file
153
docs/drivers/AbCip-Performance.md
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# 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.
|
||||||
@@ -96,6 +96,15 @@ value per PR 10, but `ab_server` accepts whatever the client asks for — the
|
|||||||
cap's correctness is trusted from its unit test, never stressed against a
|
cap's correctness is trusted from its unit test, never stressed against a
|
||||||
simulator that rejects oversized requests.
|
simulator that rejects oversized requests.
|
||||||
|
|
||||||
|
PR abcip-3.1 layers the **per-device `ConnectionSize` override** on top
|
||||||
|
(`AbCipDeviceOptions.ConnectionSize`, range `[500..4002]`, see
|
||||||
|
[`AbCip-Performance.md`](AbCip-Performance.md)). Same gap — `ab_server`
|
||||||
|
happily honours an oversized override against the CompactLogix profile, so
|
||||||
|
the legacy-firmware warning + Forward Open rejection that real 5069-L1/L2/L3
|
||||||
|
parts emit are unit-tested only. Live coverage stays Emulate / rig-only
|
||||||
|
(connect against a real CompactLogix L2 with `ConnectionSize=1500` to
|
||||||
|
confirm the Forward Open fails with CIP error 0x01/0x113).
|
||||||
|
|
||||||
### 6. BOOL-within-DINT read-modify-write (#181)
|
### 6. BOOL-within-DINT read-modify-write (#181)
|
||||||
|
|
||||||
The `AbCipDriver.WriteBitInDIntAsync` RMW path + its per-parent `SemaphoreSlim`
|
The `AbCipDriver.WriteBitInDIntAsync` RMW path + its per-parent `SemaphoreSlim`
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ VALUES (@Gen, @EqId, @EqUuid, @DrvId, @LineId, 'ab-sim', 'abcip-001', 1);
|
|||||||
|
|
||||||
-- AB CIP DriverInstance — single ControlLogix device at the ab_server fixture
|
-- AB CIP DriverInstance — single ControlLogix device at the ab_server fixture
|
||||||
-- gateway. DriverConfig shape mirrors AbCipDriverConfigDto.
|
-- gateway. DriverConfig shape mirrors AbCipDriverConfigDto.
|
||||||
|
--
|
||||||
|
-- The second device entry (CompactLogix L2 example, commented out) demonstrates
|
||||||
|
-- the PR abcip-3.1 ConnectionSize override knob. Uncomment + point at a real
|
||||||
|
-- 5069-L2 to verify the narrow-buffer Forward Open path; ab_server itself
|
||||||
|
-- doesn't enforce the narrow cap (see docs/drivers/AbServer-Test-Fixture.md §5).
|
||||||
INSERT dbo.DriverInstance(GenerationId, DriverInstanceId, ClusterId, NamespaceId,
|
INSERT dbo.DriverInstance(GenerationId, DriverInstanceId, ClusterId, NamespaceId,
|
||||||
Name, DriverType, DriverConfig, Enabled)
|
Name, DriverType, DriverConfig, Enabled)
|
||||||
VALUES (@Gen, @DrvId, @ClusterId, @NsId, 'ab-server-smoke', 'AbCip', N'{
|
VALUES (@Gen, @DrvId, @ClusterId, @NsId, 'ab-server-smoke', 'AbCip', N'{
|
||||||
@@ -90,6 +95,14 @@ VALUES (@Gen, @DrvId, @ClusterId, @NsId, 'ab-server-smoke', 'AbCip', N'{
|
|||||||
"PlcFamily": "ControlLogix",
|
"PlcFamily": "ControlLogix",
|
||||||
"DeviceName": "ab-server"
|
"DeviceName": "ab-server"
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
, {
|
||||||
|
"HostAddress": "ab://10.0.0.7/1,0",
|
||||||
|
"PlcFamily": "CompactLogix",
|
||||||
|
"DeviceName": "compactlogix-l2-narrow",
|
||||||
|
"ConnectionSize": 504
|
||||||
|
}
|
||||||
|
*/
|
||||||
],
|
],
|
||||||
"Probe": { "Enabled": true, "IntervalMs": 5000, "TimeoutMs": 2000 },
|
"Probe": { "Enabled": true, "IntervalMs": 5000, "TimeoutMs": 2000 },
|
||||||
"Tags": [
|
"Tags": [
|
||||||
|
|||||||
29
src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipConnectionSize.cs
Normal file
29
src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipConnectionSize.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PR abcip-3.1 — bounds + magic numbers for the per-device CIP <c>ConnectionSize</c>
|
||||||
|
/// override. Pulled into a single place so config validation, the legacy-firmware warning,
|
||||||
|
/// and the docs stay in sync.
|
||||||
|
/// </summary>
|
||||||
|
public static class AbCipConnectionSize
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum supported CIP Forward Open buffer size, in bytes. Matches the lower bound of
|
||||||
|
/// Kepware's connection-size slider for ControlLogix drivers + the libplctag native
|
||||||
|
/// floor that still leaves headroom for the CIP MR header.
|
||||||
|
/// </summary>
|
||||||
|
public const int Min = 500;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum supported CIP Forward Open buffer size, in bytes. Matches the upper bound of
|
||||||
|
/// Kepware's slider + the Large Forward Open ceiling on FW20+ ControlLogix.
|
||||||
|
/// </summary>
|
||||||
|
public const int Max = 4002;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Soft cap above which legacy ControlLogix firmware (v19 and earlier) rejects the
|
||||||
|
/// Forward Open. CompactLogix L1/L2/L3 narrow-cap parts (5069-L1/L2/L3) and Micro800
|
||||||
|
/// hard-cap below this too. Used as the threshold for the legacy-firmware warning.
|
||||||
|
/// </summary>
|
||||||
|
public const int LegacyFirmwareCap = 511;
|
||||||
|
}
|
||||||
@@ -83,7 +83,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
CipPath: device.ParsedAddress.CipPath,
|
CipPath: device.ParsedAddress.CipPath,
|
||||||
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
||||||
TagName: $"@udt/{templateInstanceId}",
|
TagName: $"@udt/{templateInstanceId}",
|
||||||
Timeout: _options.Timeout);
|
Timeout: _options.Timeout,
|
||||||
|
ConnectionSize: device.ConnectionSize);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -121,6 +122,31 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
?? throw new InvalidOperationException(
|
?? throw new InvalidOperationException(
|
||||||
$"AbCip device has invalid HostAddress '{device.HostAddress}' — expected 'ab://gateway[:port]/cip-path'.");
|
$"AbCip device has invalid HostAddress '{device.HostAddress}' — expected 'ab://gateway[:port]/cip-path'.");
|
||||||
var profile = AbCipPlcFamilyProfile.ForFamily(device.PlcFamily);
|
var profile = AbCipPlcFamilyProfile.ForFamily(device.PlcFamily);
|
||||||
|
// PR abcip-3.1 — validate the optional ConnectionSize override before stamping
|
||||||
|
// the device. The Kepware-supported range [500..4002] is what the libplctag
|
||||||
|
// ControlLogix driver supports; anything outside that fails the Forward Open at
|
||||||
|
// runtime, so we reject it loudly at config time instead.
|
||||||
|
if (device.ConnectionSize is int explicitSize)
|
||||||
|
{
|
||||||
|
if (explicitSize < AbCipConnectionSize.Min || explicitSize > AbCipConnectionSize.Max)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"AbCip device '{device.HostAddress}' has ConnectionSize {explicitSize} outside the supported range " +
|
||||||
|
$"[{AbCipConnectionSize.Min}..{AbCipConnectionSize.Max}].");
|
||||||
|
// Legacy-firmware warning: families whose profile default is 504 (CompactLogix
|
||||||
|
// narrow cap, also where v19-and-earlier ControlLogix lives) can't actually
|
||||||
|
// raise their CIP buffer above 511 bytes — the controller rejects the Forward
|
||||||
|
// Open. Ship the override anyway so newer firmware can use it, but flag the
|
||||||
|
// mismatch so operators see it in the warning sink.
|
||||||
|
if (explicitSize > AbCipConnectionSize.LegacyFirmwareCap
|
||||||
|
&& profile.DefaultConnectionSize <= AbCipConnectionSize.LegacyFirmwareCap)
|
||||||
|
{
|
||||||
|
_options.OnWarning?.Invoke(
|
||||||
|
$"AbCip device '{device.HostAddress}' family '{device.PlcFamily}' uses a narrow-buffer profile " +
|
||||||
|
$"(default ConnectionSize {profile.DefaultConnectionSize}); the configured ConnectionSize {explicitSize} " +
|
||||||
|
$"exceeds the {AbCipConnectionSize.LegacyFirmwareCap}-byte legacy-firmware cap and will fail the " +
|
||||||
|
"Forward Open on v19-and-earlier ControlLogix or 5069-L1/L2/L3 CompactLogix firmware.");
|
||||||
|
}
|
||||||
|
}
|
||||||
_devices[device.HostAddress] = new DeviceState(addr, device, profile);
|
_devices[device.HostAddress] = new DeviceState(addr, device, profile);
|
||||||
}
|
}
|
||||||
// Pre-declared tags first; L5K imports fill in only the names not already covered
|
// Pre-declared tags first; L5K imports fill in only the names not already covered
|
||||||
@@ -356,7 +382,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
CipPath: state.ParsedAddress.CipPath,
|
CipPath: state.ParsedAddress.CipPath,
|
||||||
LibplctagPlcAttribute: state.Profile.LibplctagPlcAttribute,
|
LibplctagPlcAttribute: state.Profile.LibplctagPlcAttribute,
|
||||||
TagName: _options.Probe.ProbeTagPath!,
|
TagName: _options.Probe.ProbeTagPath!,
|
||||||
Timeout: _options.Probe.Timeout);
|
Timeout: _options.Probe.Timeout,
|
||||||
|
ConnectionSize: state.ConnectionSize);
|
||||||
|
|
||||||
IAbCipTagRuntime? probeRuntime = null;
|
IAbCipTagRuntime? probeRuntime = null;
|
||||||
while (!ct.IsCancellationRequested)
|
while (!ct.IsCancellationRequested)
|
||||||
@@ -533,7 +560,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
CipPath: device.ParsedAddress.CipPath,
|
CipPath: device.ParsedAddress.CipPath,
|
||||||
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
||||||
TagName: parsedPath.ToLibplctagName(),
|
TagName: parsedPath.ToLibplctagName(),
|
||||||
Timeout: _options.Timeout);
|
Timeout: _options.Timeout,
|
||||||
|
ConnectionSize: device.ConnectionSize);
|
||||||
|
|
||||||
var plan = AbCipArrayReadPlanner.TryBuild(def, parsedPath, baseParams);
|
var plan = AbCipArrayReadPlanner.TryBuild(def, parsedPath, baseParams);
|
||||||
if (plan is null)
|
if (plan is null)
|
||||||
@@ -892,7 +920,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
CipPath: device.ParsedAddress.CipPath,
|
CipPath: device.ParsedAddress.CipPath,
|
||||||
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
||||||
TagName: parentTagName,
|
TagName: parentTagName,
|
||||||
Timeout: _options.Timeout));
|
Timeout: _options.Timeout,
|
||||||
|
ConnectionSize: device.ConnectionSize));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await runtime.InitializeAsync(ct).ConfigureAwait(false);
|
await runtime.InitializeAsync(ct).ConfigureAwait(false);
|
||||||
@@ -927,7 +956,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
||||||
TagName: parsed.ToLibplctagName(),
|
TagName: parsed.ToLibplctagName(),
|
||||||
Timeout: _options.Timeout,
|
Timeout: _options.Timeout,
|
||||||
StringMaxCapacity: def.DataType == AbCipDataType.String ? def.StringLength : null));
|
StringMaxCapacity: def.DataType == AbCipDataType.String ? def.StringLength : null,
|
||||||
|
ConnectionSize: device.ConnectionSize));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await runtime.InitializeAsync(ct).ConfigureAwait(false);
|
await runtime.InitializeAsync(ct).ConfigureAwait(false);
|
||||||
@@ -1081,7 +1111,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
CipPath: state.ParsedAddress.CipPath,
|
CipPath: state.ParsedAddress.CipPath,
|
||||||
LibplctagPlcAttribute: state.Profile.LibplctagPlcAttribute,
|
LibplctagPlcAttribute: state.Profile.LibplctagPlcAttribute,
|
||||||
TagName: "@tags",
|
TagName: "@tags",
|
||||||
Timeout: _options.Timeout);
|
Timeout: _options.Timeout,
|
||||||
|
ConnectionSize: state.ConnectionSize);
|
||||||
|
|
||||||
IAddressSpaceBuilder? discoveredFolder = null;
|
IAddressSpaceBuilder? discoveredFolder = null;
|
||||||
await foreach (var discovered in enumerator.EnumerateAsync(deviceParams, cancellationToken)
|
await foreach (var discovered in enumerator.EnumerateAsync(deviceParams, cancellationToken)
|
||||||
@@ -1152,6 +1183,16 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
public AbCipDeviceOptions Options { get; } = options;
|
public AbCipDeviceOptions Options { get; } = options;
|
||||||
public AbCipPlcFamilyProfile Profile { get; } = profile;
|
public AbCipPlcFamilyProfile Profile { get; } = profile;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PR abcip-3.1 — effective CIP connection size for this device. Per-device
|
||||||
|
/// <see cref="AbCipDeviceOptions.ConnectionSize"/> override wins; otherwise the
|
||||||
|
/// family profile's <see cref="AbCipPlcFamilyProfile.DefaultConnectionSize"/>
|
||||||
|
/// (4002 / 504 / 488 depending on family). Threaded through every
|
||||||
|
/// <see cref="AbCipTagCreateParams"/> the driver builds so libplctag receives a
|
||||||
|
/// consistent buffer-size hint across read / write / probe / discovery handles.
|
||||||
|
/// </summary>
|
||||||
|
public int ConnectionSize { get; } = options.ConnectionSize ?? profile.DefaultConnectionSize;
|
||||||
|
|
||||||
public object ProbeLock { get; } = new();
|
public object ProbeLock { get; } = new();
|
||||||
public HostState HostState { get; set; } = HostState.Unknown;
|
public HostState HostState { get; set; } = HostState.Unknown;
|
||||||
public DateTime HostStateChangedUtc { get; set; } = DateTime.UtcNow;
|
public DateTime HostStateChangedUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ public static class AbCipDriverFactoryExtensions
|
|||||||
$"AB CIP config for '{driverInstanceId}' has a device missing HostAddress"),
|
$"AB CIP config for '{driverInstanceId}' has a device missing HostAddress"),
|
||||||
PlcFamily: ParseEnum<AbCipPlcFamily>(d.PlcFamily, "device", driverInstanceId, "PlcFamily",
|
PlcFamily: ParseEnum<AbCipPlcFamily>(d.PlcFamily, "device", driverInstanceId, "PlcFamily",
|
||||||
fallback: AbCipPlcFamily.ControlLogix),
|
fallback: AbCipPlcFamily.ControlLogix),
|
||||||
DeviceName: d.DeviceName))]
|
DeviceName: d.DeviceName,
|
||||||
|
ConnectionSize: d.ConnectionSize))]
|
||||||
: [],
|
: [],
|
||||||
Tags = dto.Tags is { Count: > 0 }
|
Tags = dto.Tags is { Count: > 0 }
|
||||||
? [.. dto.Tags.Select(t => BuildTag(t, driverInstanceId))]
|
? [.. dto.Tags.Select(t => BuildTag(t, driverInstanceId))]
|
||||||
@@ -119,6 +120,12 @@ public static class AbCipDriverFactoryExtensions
|
|||||||
public string? HostAddress { get; init; }
|
public string? HostAddress { get; init; }
|
||||||
public string? PlcFamily { get; init; }
|
public string? PlcFamily { get; init; }
|
||||||
public string? DeviceName { get; init; }
|
public string? DeviceName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PR abcip-3.1 — optional per-device CIP <c>ConnectionSize</c> override. Validated
|
||||||
|
/// against <c>[500..4002]</c> at <see cref="AbCipDriver.InitializeAsync"/>.
|
||||||
|
/// </summary>
|
||||||
|
public int? ConnectionSize { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class AbCipTagDto
|
internal sealed class AbCipTagDto
|
||||||
|
|||||||
@@ -87,6 +87,14 @@ public sealed class AbCipDriverOptions
|
|||||||
/// 1 second — matches typical SCADA alarm-refresh conventions.
|
/// 1 second — matches typical SCADA alarm-refresh conventions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan AlarmPollInterval { get; init; } = TimeSpan.FromSeconds(1);
|
public TimeSpan AlarmPollInterval { get; init; } = TimeSpan.FromSeconds(1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PR abcip-3.1 — optional sink for non-fatal driver warnings (legacy-firmware
|
||||||
|
/// <c>ConnectionSize</c> mis-match, etc.). Production hosting wires this to Serilog;
|
||||||
|
/// unit tests pin a list-collecting lambda to assert which warnings fired. <c>null</c>
|
||||||
|
/// swallows warnings — convenient for back-compat deployments that don't care.
|
||||||
|
/// </summary>
|
||||||
|
public Action<string>? OnWarning { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -98,10 +106,19 @@ public sealed class AbCipDriverOptions
|
|||||||
/// <param name="PlcFamily">Which per-family profile to apply. Determines ConnectionSize,
|
/// <param name="PlcFamily">Which per-family profile to apply. Determines ConnectionSize,
|
||||||
/// request-packing support, unconnected-only hint, and other quirks.</param>
|
/// request-packing support, unconnected-only hint, and other quirks.</param>
|
||||||
/// <param name="DeviceName">Optional display label for Admin UI. Falls back to <see cref="HostAddress"/>.</param>
|
/// <param name="DeviceName">Optional display label for Admin UI. Falls back to <see cref="HostAddress"/>.</param>
|
||||||
|
/// <param name="ConnectionSize">PR abcip-3.1 — optional override for the family-default
|
||||||
|
/// <see cref="PlcFamilies.AbCipPlcFamilyProfile.DefaultConnectionSize"/>. Threads through to
|
||||||
|
/// libplctag's <c>connection_size</c> attribute on the underlying tag handle so operators can
|
||||||
|
/// dial the CIP Forward Open buffer down for legacy firmware (v19-and-earlier ControlLogix
|
||||||
|
/// caps at 504) or up for high-throughput shops on FW20+. Validated against the Kepware
|
||||||
|
/// supported range [500..4002] at <c>InitializeAsync</c>; out-of-range values fault the
|
||||||
|
/// driver. <c>null</c> uses the family default — back-compat with deployments that haven't
|
||||||
|
/// touched the knob.</param>
|
||||||
public sealed record AbCipDeviceOptions(
|
public sealed record AbCipDeviceOptions(
|
||||||
string HostAddress,
|
string HostAddress,
|
||||||
AbCipPlcFamily PlcFamily = AbCipPlcFamily.ControlLogix,
|
AbCipPlcFamily PlcFamily = AbCipPlcFamily.ControlLogix,
|
||||||
string? DeviceName = null);
|
string? DeviceName = null,
|
||||||
|
int? ConnectionSize = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// One AB-backed OPC UA variable. Mirrors the <c>ModbusTagDefinition</c> shape.
|
/// One AB-backed OPC UA variable. Mirrors the <c>ModbusTagDefinition</c> shape.
|
||||||
|
|||||||
@@ -73,6 +73,13 @@ public interface IAbCipTagFactory
|
|||||||
/// to issue a Rockwell array read covering <c>N</c> consecutive elements starting at the
|
/// to issue a Rockwell array read covering <c>N</c> consecutive elements starting at the
|
||||||
/// subscripted index in <see cref="TagName"/>. Drives PR abcip-1.3 array-slice support;
|
/// subscripted index in <see cref="TagName"/>. Drives PR abcip-1.3 array-slice support;
|
||||||
/// <c>null</c> leaves libplctag's default scalar-element behaviour for back-compat.</param>
|
/// <c>null</c> leaves libplctag's default scalar-element behaviour for back-compat.</param>
|
||||||
|
/// <param name="ConnectionSize">PR abcip-3.1 — CIP Forward Open buffer size in bytes. Threads
|
||||||
|
/// through to libplctag's <c>connection_size</c> attribute. The driver always supplies a
|
||||||
|
/// value here — either the per-device <see cref="AbCipDeviceOptions.ConnectionSize"/>
|
||||||
|
/// override or the family profile's <see cref="PlcFamilies.AbCipPlcFamilyProfile.DefaultConnectionSize"/>.
|
||||||
|
/// Bigger packets fit more tags per RTT (higher throughput); smaller packets stay compatible
|
||||||
|
/// with legacy firmware (v19-and-earlier ControlLogix caps at 504, Micro800 hard-caps at
|
||||||
|
/// 488).</param>
|
||||||
public sealed record AbCipTagCreateParams(
|
public sealed record AbCipTagCreateParams(
|
||||||
string Gateway,
|
string Gateway,
|
||||||
int Port,
|
int Port,
|
||||||
@@ -81,4 +88,5 @@ public sealed record AbCipTagCreateParams(
|
|||||||
string TagName,
|
string TagName,
|
||||||
TimeSpan Timeout,
|
TimeSpan Timeout,
|
||||||
int? StringMaxCapacity = null,
|
int? StringMaxCapacity = null,
|
||||||
int? ElementCount = null);
|
int? ElementCount = null,
|
||||||
|
int ConnectionSize = 4002);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Reflection;
|
||||||
using libplctag;
|
using libplctag;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||||
@@ -12,6 +13,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
|||||||
internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
|
internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
|
||||||
{
|
{
|
||||||
private readonly Tag _tag;
|
private readonly Tag _tag;
|
||||||
|
private readonly int _connectionSize;
|
||||||
|
|
||||||
public LibplctagTagRuntime(AbCipTagCreateParams p)
|
public LibplctagTagRuntime(AbCipTagCreateParams p)
|
||||||
{
|
{
|
||||||
@@ -35,12 +37,55 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
|
|||||||
// to issue one Rockwell array read for a [N..M] slice.
|
// to issue one Rockwell array read for a [N..M] slice.
|
||||||
if (p.ElementCount is int n && n > 0)
|
if (p.ElementCount is int n && n > 0)
|
||||||
_tag.ElementCount = n;
|
_tag.ElementCount = n;
|
||||||
|
_connectionSize = p.ConnectionSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InitializeAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _tag.InitializeAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
// PR abcip-3.1 — propagate the configured CIP connection size to the native libplctag
|
||||||
|
// handle. The 1.5.x C# wrapper does not expose <c>connection_size</c> as a public Tag
|
||||||
|
// property, so we reach into the internal <c>NativeTagWrapper</c>'s
|
||||||
|
// <c>SetIntAttribute</c> (mirroring libplctag's <c>plc_tag_set_int_attribute</c>).
|
||||||
|
// libplctag native parses <c>connection_size</c> at create time, so this best-effort
|
||||||
|
// call lights up automatically when a future wrapper release exposes the attribute or
|
||||||
|
// when libplctag native gains post-create hot-update support — until then it falls back
|
||||||
|
// to the wrapper default. Failures (older / patched wrappers without the internal API)
|
||||||
|
// are intentionally swallowed so the driver keeps initialising.
|
||||||
|
TrySetConnectionSize(_tag, _connectionSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task InitializeAsync(CancellationToken cancellationToken) => _tag.InitializeAsync(cancellationToken);
|
|
||||||
public Task ReadAsync(CancellationToken cancellationToken) => _tag.ReadAsync(cancellationToken);
|
public Task ReadAsync(CancellationToken cancellationToken) => _tag.ReadAsync(cancellationToken);
|
||||||
public Task WriteAsync(CancellationToken cancellationToken) => _tag.WriteAsync(cancellationToken);
|
public Task WriteAsync(CancellationToken cancellationToken) => _tag.WriteAsync(cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Best-effort propagation of <c>connection_size</c> to libplctag native. Reflects into
|
||||||
|
/// the wrapper's internal <c>NativeTagWrapper.SetIntAttribute(string, int)</c>; isolated
|
||||||
|
/// in a static helper so the lookup costs run once + the failure path is one line.
|
||||||
|
/// </summary>
|
||||||
|
private static void TrySetConnectionSize(Tag tag, int connectionSize)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var wrapperField = typeof(Tag).GetField("_tag", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
var wrapper = wrapperField?.GetValue(tag);
|
||||||
|
if (wrapper is null) return;
|
||||||
|
var setInt = wrapper.GetType().GetMethod(
|
||||||
|
"SetIntAttribute",
|
||||||
|
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
|
||||||
|
binder: null,
|
||||||
|
types: [typeof(string), typeof(int)],
|
||||||
|
modifiers: null);
|
||||||
|
setInt?.Invoke(wrapper, ["connection_size", connectionSize]);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Wrapper internals shifted (newer libplctag.NET) — drop quietly. Either the new
|
||||||
|
// wrapper exposes ConnectionSize directly (our reflection no-ops) or operators must
|
||||||
|
// upgrade to a known-good version per docs/drivers/AbCip-Performance.md.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public int GetStatus() => (int)_tag.GetStatus();
|
public int GetStatus() => (int)_tag.GetStatus();
|
||||||
|
|
||||||
public object? DecodeValue(AbCipDataType type, int? bitIndex) => DecodeValueAt(type, 0, bitIndex);
|
public object? DecodeValue(AbCipDataType type, int? bitIndex) => DecodeValueAt(type, 0, bitIndex);
|
||||||
|
|||||||
@@ -0,0 +1,329 @@
|
|||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Driver.AbCip.PlcFamilies;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PR abcip-3.1 — coverage for the per-device CIP <c>ConnectionSize</c> override.
|
||||||
|
/// Asserts (a) the value flows from <see cref="AbCipDeviceOptions"/> into every
|
||||||
|
/// <see cref="AbCipTagCreateParams"/> the driver builds, (b) the family default kicks in
|
||||||
|
/// when the override is unset, (c) values outside the Kepware-supported range are rejected
|
||||||
|
/// at <c>InitializeAsync</c>, and (d) the legacy-firmware warning fires when a CompactLogix
|
||||||
|
/// narrow-cap device is configured above 511 bytes.
|
||||||
|
/// </summary>
|
||||||
|
[Trait("Category", "Unit")]
|
||||||
|
public sealed class AbCipConnectionSizeTests
|
||||||
|
{
|
||||||
|
// ---- options threading ----
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Custom_ConnectionSize_flows_from_device_options_into_create_params()
|
||||||
|
{
|
||||||
|
var factory = new FakeAbCipTagFactory();
|
||||||
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||||
|
{
|
||||||
|
Devices =
|
||||||
|
[
|
||||||
|
new AbCipDeviceOptions(
|
||||||
|
HostAddress: "ab://10.0.0.5/1,0",
|
||||||
|
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||||
|
ConnectionSize: 1500),
|
||||||
|
],
|
||||||
|
Probe = new AbCipProbeOptions { Enabled = false },
|
||||||
|
Tags =
|
||||||
|
[
|
||||||
|
new AbCipTagDefinition(
|
||||||
|
Name: "Speed",
|
||||||
|
DeviceHostAddress: "ab://10.0.0.5/1,0",
|
||||||
|
TagPath: "Speed",
|
||||||
|
DataType: AbCipDataType.DInt),
|
||||||
|
],
|
||||||
|
}, "drv-1", tagFactory: factory);
|
||||||
|
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||||
|
|
||||||
|
factory.Tags["Speed"].CreationParams.ConnectionSize.ShouldBe(1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Unset_ConnectionSize_falls_back_to_ControlLogix_family_default()
|
||||||
|
{
|
||||||
|
var factory = new FakeAbCipTagFactory();
|
||||||
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||||
|
{
|
||||||
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0", AbCipPlcFamily.ControlLogix)],
|
||||||
|
Probe = new AbCipProbeOptions { Enabled = false },
|
||||||
|
Tags =
|
||||||
|
[
|
||||||
|
new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt),
|
||||||
|
],
|
||||||
|
}, "drv-1", tagFactory: factory);
|
||||||
|
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||||
|
|
||||||
|
factory.Tags["Speed"].CreationParams.ConnectionSize
|
||||||
|
.ShouldBe(AbCipPlcFamilyProfile.ControlLogix.DefaultConnectionSize);
|
||||||
|
factory.Tags["Speed"].CreationParams.ConnectionSize.ShouldBe(4002);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Unset_ConnectionSize_falls_back_to_CompactLogix_family_default()
|
||||||
|
{
|
||||||
|
var factory = new FakeAbCipTagFactory();
|
||||||
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||||
|
{
|
||||||
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0", AbCipPlcFamily.CompactLogix)],
|
||||||
|
Probe = new AbCipProbeOptions { Enabled = false },
|
||||||
|
Tags =
|
||||||
|
[
|
||||||
|
new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt),
|
||||||
|
],
|
||||||
|
}, "drv-1", tagFactory: factory);
|
||||||
|
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||||
|
|
||||||
|
factory.Tags["Speed"].CreationParams.ConnectionSize.ShouldBe(504);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Unset_ConnectionSize_falls_back_to_Micro800_family_default()
|
||||||
|
{
|
||||||
|
var factory = new FakeAbCipTagFactory();
|
||||||
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||||
|
{
|
||||||
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.6/", AbCipPlcFamily.Micro800)],
|
||||||
|
Probe = new AbCipProbeOptions { Enabled = false },
|
||||||
|
Tags =
|
||||||
|
[
|
||||||
|
new AbCipTagDefinition("Speed", "ab://10.0.0.6/", "Speed", AbCipDataType.DInt),
|
||||||
|
],
|
||||||
|
}, "drv-1", tagFactory: factory);
|
||||||
|
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||||
|
|
||||||
|
factory.Tags["Speed"].CreationParams.ConnectionSize.ShouldBe(488);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- range validation ----
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(499)]
|
||||||
|
[InlineData(0)]
|
||||||
|
[InlineData(-1)]
|
||||||
|
[InlineData(4003)]
|
||||||
|
[InlineData(10000)]
|
||||||
|
public async Task Out_of_range_ConnectionSize_throws_at_InitializeAsync(int badSize)
|
||||||
|
{
|
||||||
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||||
|
{
|
||||||
|
Devices =
|
||||||
|
[
|
||||||
|
new AbCipDeviceOptions(
|
||||||
|
HostAddress: "ab://10.0.0.5/1,0",
|
||||||
|
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||||
|
ConnectionSize: badSize),
|
||||||
|
],
|
||||||
|
Probe = new AbCipProbeOptions { Enabled = false },
|
||||||
|
}, "drv-1");
|
||||||
|
|
||||||
|
var ex = await Should.ThrowAsync<InvalidOperationException>(
|
||||||
|
() => drv.InitializeAsync("{}", CancellationToken.None));
|
||||||
|
ex.Message.ShouldContain("ConnectionSize");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(500)]
|
||||||
|
[InlineData(504)]
|
||||||
|
[InlineData(2000)]
|
||||||
|
[InlineData(4002)]
|
||||||
|
public async Task In_range_ConnectionSize_initialises_cleanly(int goodSize)
|
||||||
|
{
|
||||||
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||||
|
{
|
||||||
|
Devices =
|
||||||
|
[
|
||||||
|
new AbCipDeviceOptions(
|
||||||
|
HostAddress: "ab://10.0.0.5/1,0",
|
||||||
|
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||||
|
ConnectionSize: goodSize),
|
||||||
|
],
|
||||||
|
Probe = new AbCipProbeOptions { Enabled = false },
|
||||||
|
}, "drv-1");
|
||||||
|
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
drv.GetDeviceState("ab://10.0.0.5/1,0")!.ConnectionSize.ShouldBe(goodSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- legacy-firmware warning ----
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Oversized_ConnectionSize_on_CompactLogix_emits_legacy_warning()
|
||||||
|
{
|
||||||
|
var warnings = new List<string>();
|
||||||
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||||
|
{
|
||||||
|
Devices =
|
||||||
|
[
|
||||||
|
new AbCipDeviceOptions(
|
||||||
|
HostAddress: "ab://10.0.0.5/1,0",
|
||||||
|
PlcFamily: AbCipPlcFamily.CompactLogix,
|
||||||
|
ConnectionSize: 1500),
|
||||||
|
],
|
||||||
|
Probe = new AbCipProbeOptions { Enabled = false },
|
||||||
|
OnWarning = warnings.Add,
|
||||||
|
}, "drv-1");
|
||||||
|
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
|
||||||
|
warnings.ShouldHaveSingleItem();
|
||||||
|
warnings[0].ShouldContain("CompactLogix");
|
||||||
|
warnings[0].ShouldContain("1500");
|
||||||
|
warnings[0].ShouldContain("Forward Open");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Within_legacy_cap_on_CompactLogix_does_not_warn()
|
||||||
|
{
|
||||||
|
var warnings = new List<string>();
|
||||||
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||||
|
{
|
||||||
|
Devices =
|
||||||
|
[
|
||||||
|
new AbCipDeviceOptions(
|
||||||
|
HostAddress: "ab://10.0.0.5/1,0",
|
||||||
|
PlcFamily: AbCipPlcFamily.CompactLogix,
|
||||||
|
ConnectionSize: 504),
|
||||||
|
],
|
||||||
|
Probe = new AbCipProbeOptions { Enabled = false },
|
||||||
|
OnWarning = warnings.Add,
|
||||||
|
}, "drv-1");
|
||||||
|
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
|
||||||
|
warnings.ShouldBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Oversized_ConnectionSize_on_ControlLogix_does_not_warn()
|
||||||
|
{
|
||||||
|
// ControlLogix profile default is 4002 (Large Forward Open) — the warning is only
|
||||||
|
// meaningful when the family default is in the legacy-cap bucket. FW20+ ControlLogix
|
||||||
|
// happily accepts 1500-byte connections, so no warning fires.
|
||||||
|
var warnings = new List<string>();
|
||||||
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||||
|
{
|
||||||
|
Devices =
|
||||||
|
[
|
||||||
|
new AbCipDeviceOptions(
|
||||||
|
HostAddress: "ab://10.0.0.5/1,0",
|
||||||
|
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||||
|
ConnectionSize: 1500),
|
||||||
|
],
|
||||||
|
Probe = new AbCipProbeOptions { Enabled = false },
|
||||||
|
OnWarning = warnings.Add,
|
||||||
|
}, "drv-1");
|
||||||
|
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
|
||||||
|
warnings.ShouldBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Oversized_ConnectionSize_on_Micro800_emits_legacy_warning()
|
||||||
|
{
|
||||||
|
// Micro800 default is 488 (well under the legacy cap), so any over-511 override
|
||||||
|
// triggers the same family-mismatch warning.
|
||||||
|
var warnings = new List<string>();
|
||||||
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||||
|
{
|
||||||
|
Devices =
|
||||||
|
[
|
||||||
|
new AbCipDeviceOptions(
|
||||||
|
HostAddress: "ab://10.0.0.6/",
|
||||||
|
PlcFamily: AbCipPlcFamily.Micro800,
|
||||||
|
ConnectionSize: 1000),
|
||||||
|
],
|
||||||
|
Probe = new AbCipProbeOptions { Enabled = false },
|
||||||
|
OnWarning = warnings.Add,
|
||||||
|
}, "drv-1");
|
||||||
|
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
|
||||||
|
warnings.ShouldHaveSingleItem();
|
||||||
|
warnings[0].ShouldContain("Micro800");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- DeviceState resolved ConnectionSize ----
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeviceState_ConnectionSize_reflects_override_when_set()
|
||||||
|
{
|
||||||
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||||
|
{
|
||||||
|
Devices =
|
||||||
|
[
|
||||||
|
new AbCipDeviceOptions("ab://10.0.0.5/1,0", AbCipPlcFamily.ControlLogix, ConnectionSize: 2000),
|
||||||
|
],
|
||||||
|
Probe = new AbCipProbeOptions { Enabled = false },
|
||||||
|
}, "drv-1");
|
||||||
|
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
drv.GetDeviceState("ab://10.0.0.5/1,0")!.ConnectionSize.ShouldBe(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeviceState_ConnectionSize_reflects_family_default_when_unset()
|
||||||
|
{
|
||||||
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||||
|
{
|
||||||
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0", AbCipPlcFamily.CompactLogix)],
|
||||||
|
Probe = new AbCipProbeOptions { Enabled = false },
|
||||||
|
}, "drv-1");
|
||||||
|
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
drv.GetDeviceState("ab://10.0.0.5/1,0")!.ConnectionSize.ShouldBe(504);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- AbCipConnectionSize constants ----
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constants_match_documented_Kepware_range()
|
||||||
|
{
|
||||||
|
AbCipConnectionSize.Min.ShouldBe(500);
|
||||||
|
AbCipConnectionSize.Max.ShouldBe(4002);
|
||||||
|
AbCipConnectionSize.LegacyFirmwareCap.ShouldBe(511);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- DriverConfig DTO path (DriverFactoryRegistry-bound deployments) ----
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Driver_factory_threads_ConnectionSize_through_config_json()
|
||||||
|
{
|
||||||
|
// The bootstrapper-driven path deserialises driver config from JSON in the central
|
||||||
|
// DB (sp_PublishGeneration → DriverInstance.DriverConfig). The DTO must surface
|
||||||
|
// ConnectionSize so production deployments don't lose the override at the wire.
|
||||||
|
var json = """
|
||||||
|
{
|
||||||
|
"Devices": [
|
||||||
|
{
|
||||||
|
"HostAddress": "ab://10.0.0.5/1,0",
|
||||||
|
"PlcFamily": "ControlLogix",
|
||||||
|
"ConnectionSize": 1500
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Probe": { "Enabled": false }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
var drv = AbCipDriverFactoryExtensions.CreateInstance("drv-1", json);
|
||||||
|
// CreateInstance returns a fully-built driver; we kick InitializeAsync to surface the
|
||||||
|
// resolved DeviceState.ConnectionSize.
|
||||||
|
await drv.InitializeAsync(json, CancellationToken.None);
|
||||||
|
drv.GetDeviceState("ab://10.0.0.5/1,0")!.ConnectionSize.ShouldBe(1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user