@@ -20,6 +20,7 @@ dotnet run --project src/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli -- --help
|
|||||||
| `-g` / `--gateway` | **required** | Canonical `ab://host[:port]/cip-path` |
|
| `-g` / `--gateway` | **required** | Canonical `ab://host[:port]/cip-path` |
|
||||||
| `-f` / `--family` | `ControlLogix` | ControlLogix / CompactLogix / Micro800 / GuardLogix |
|
| `-f` / `--family` | `ControlLogix` | ControlLogix / CompactLogix / Micro800 / GuardLogix |
|
||||||
| `--timeout-ms` | `5000` | Per-operation timeout |
|
| `--timeout-ms` | `5000` | Per-operation timeout |
|
||||||
|
| `--addressing-mode` | `Auto` | `Auto` / `Symbolic` / `Logical` — see [AbCip-Performance §Addressing mode](drivers/AbCip-Performance.md#addressing-mode). `Logical` against Micro800 silently falls back to Symbolic with a warning. |
|
||||||
| `--verbose` | off | Serilog debug output |
|
| `--verbose` | off | Serilog debug output |
|
||||||
|
|
||||||
Family ↔ CIP-path cheat sheet:
|
Family ↔ CIP-path cheat sheet:
|
||||||
|
|||||||
@@ -151,3 +151,158 @@ patched for runtime updates. Both paths are tracked in the AB CIP plan.
|
|||||||
per-family default values.
|
per-family default values.
|
||||||
- [`AbCipConnectionSize`](../../src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipConnectionSize.cs) —
|
- [`AbCipConnectionSize`](../../src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipConnectionSize.cs) —
|
||||||
range bounds + legacy-firmware threshold constants.
|
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]`.
|
||||||
|
|||||||
@@ -38,6 +38,16 @@ quirk. UDT / alarm / quirk behavior is verified only by unit tests with
|
|||||||
- `--plc controllogix` and `--plc compactlogix` mode dispatch.
|
- `--plc controllogix` and `--plc compactlogix` mode dispatch.
|
||||||
- The skip-on-missing-binary behavior (`AbServerFactAttribute`) so a fresh
|
- The skip-on-missing-binary behavior (`AbServerFactAttribute`) so a fresh
|
||||||
clone without the simulator stays green.
|
clone without the simulator stays green.
|
||||||
|
- **Symbolic vs Logical addressing wall-clock** (PR abcip-3.2,
|
||||||
|
`AbCipAddressingModeBenchTests`) — both modes complete + emit timing.
|
||||||
|
**Emulate-tier only**: `ab_server` does not currently honour the CIP Symbol
|
||||||
|
Object class 0x6B `cip_addr` attribute that Logical mode sets, so on the
|
||||||
|
fixture the two modes measure the same wire path. The bench scaffold
|
||||||
|
asserts both complete + records timing for human inspection; the actual
|
||||||
|
Symbolic-vs-Logical perf comparison requires a real ControlLogix /
|
||||||
|
CompactLogix on the network. See
|
||||||
|
[`docs/drivers/AbCip-Performance.md`](AbCip-Performance.md) §"Addressing
|
||||||
|
mode" for the full caveat.
|
||||||
|
|
||||||
## What it does NOT cover
|
## What it does NOT cover
|
||||||
|
|
||||||
|
|||||||
@@ -94,5 +94,30 @@ $results += Test-SubscribeSeesChange `
|
|||||||
-DriverWriteArgs (@("write") + $commonAbCip + @("-t", $TagPath, "--type", "DInt", "-v", $subValue)) `
|
-DriverWriteArgs (@("write") + $commonAbCip + @("-t", $TagPath, "--type", "DInt", "-v", $subValue)) `
|
||||||
-ExpectedValue "$subValue"
|
-ExpectedValue "$subValue"
|
||||||
|
|
||||||
|
# PR abcip-3.2 — Symbolic-vs-Logical sanity assertion. Reads the same tag with both
|
||||||
|
# addressing modes through the CLI's --addressing-mode flag. Logical-mode against ab_server
|
||||||
|
# falls back to Symbolic on the wire (libplctag wrapper limitation; see AbCip-Performance.md
|
||||||
|
# §Addressing mode), so the assertion is "both modes complete + return the same value" — not
|
||||||
|
# a perf comparison. Skipped on Micro800 (driver downgrades Logical → Symbolic with warning,
|
||||||
|
# making both reads identical-by-design + uninteresting to compare here).
|
||||||
|
if ($Family -ne "Micro800") {
|
||||||
|
$symValue = Get-Random -Minimum 40000 -Maximum 49999
|
||||||
|
Write-Host "AB CIP e2e: priming gateway with $symValue then reading via Symbolic + Logical"
|
||||||
|
$writeArgs = @("write") + $commonAbCip + @("-t", $TagPath, "--type", "DInt", "-v", $symValue)
|
||||||
|
& $abcipCli.Exe @($abcipCli.Args + $writeArgs) | Out-Null
|
||||||
|
|
||||||
|
$symRead = & $abcipCli.Exe @($abcipCli.Args + @("read") + $commonAbCip + @("-t", $TagPath, "--type", "DInt", "--addressing-mode", "Symbolic"))
|
||||||
|
$logRead = & $abcipCli.Exe @($abcipCli.Args + @("read") + $commonAbCip + @("-t", $TagPath, "--type", "DInt", "--addressing-mode", "Logical"))
|
||||||
|
|
||||||
|
$symMatched = ($symRead -join "`n") -match "$symValue"
|
||||||
|
$logMatched = ($logRead -join "`n") -match "$symValue"
|
||||||
|
$passed = $symMatched -and $logMatched
|
||||||
|
$results += [PSCustomObject]@{
|
||||||
|
Name = "AddressingModeSanity"
|
||||||
|
Passed = $passed
|
||||||
|
Detail = if ($passed) { "Symbolic + Logical both returned $symValue" } else { "Sym=$symMatched Log=$logMatched" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Write-Summary -Title "AB CIP e2e" -Results $results
|
Write-Summary -Title "AB CIP e2e" -Results $results
|
||||||
if ($results | Where-Object { -not $_.Passed }) { exit 1 }
|
if ($results | Where-Object { -not $_.Passed }) { exit 1 }
|
||||||
|
|||||||
@@ -26,6 +26,20 @@ public abstract class AbCipCommandBase : DriverCommandBase
|
|||||||
[CommandOption("timeout-ms", Description = "Per-operation timeout in ms (default 5000).")]
|
[CommandOption("timeout-ms", Description = "Per-operation timeout in ms (default 5000).")]
|
||||||
public int TimeoutMs { get; init; } = 5000;
|
public int TimeoutMs { get; init; } = 5000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PR abcip-3.2 — pin the device's CIP addressing mode for this CLI invocation.
|
||||||
|
/// Auto / Symbolic / Logical. Defaults to <see cref="AddressingMode.Auto"/> (resolves
|
||||||
|
/// to Symbolic until a future PR plumbs auto-detection). Logical against an
|
||||||
|
/// unsupported family (Micro800) silently falls back to Symbolic with a logged
|
||||||
|
/// warning, so passing <c>--addressing-mode Logical</c> across a mixed-family
|
||||||
|
/// fleet is safe.
|
||||||
|
/// </summary>
|
||||||
|
[CommandOption("addressing-mode", Description =
|
||||||
|
"CIP addressing mode: Auto / Symbolic / Logical (default Auto, resolves to " +
|
||||||
|
"Symbolic). Logical uses CIP Symbol Object instance IDs after a one-time @tags " +
|
||||||
|
"walk; unsupported on Micro800 (silent fallback to Symbolic with warning).")]
|
||||||
|
public AddressingMode AddressingMode { get; init; } = AddressingMode.Auto;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override TimeSpan Timeout
|
public override TimeSpan Timeout
|
||||||
{
|
{
|
||||||
@@ -43,7 +57,8 @@ public abstract class AbCipCommandBase : DriverCommandBase
|
|||||||
Devices = [new AbCipDeviceOptions(
|
Devices = [new AbCipDeviceOptions(
|
||||||
HostAddress: Gateway,
|
HostAddress: Gateway,
|
||||||
PlcFamily: Family,
|
PlcFamily: Family,
|
||||||
DeviceName: $"cli-{Family}")],
|
DeviceName: $"cli-{Family}",
|
||||||
|
AddressingMode: AddressingMode)],
|
||||||
Tags = tags,
|
Tags = tags,
|
||||||
Timeout = Timeout,
|
Timeout = Timeout,
|
||||||
Probe = new AbCipProbeOptions { Enabled = false },
|
Probe = new AbCipProbeOptions { Enabled = false },
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
|
|
||||||
if (!_devices.TryGetValue(deviceHostAddress, out var device)) return null;
|
if (!_devices.TryGetValue(deviceHostAddress, out var device)) return null;
|
||||||
|
|
||||||
|
// UDT template reads target the @udt/{id} pseudo-tag, which the controller already
|
||||||
|
// serves via a logical-segment path of its own. Force Symbolic addressing so we don't
|
||||||
|
// overlay the driver's Logical mode on top — libplctag knows how to dereference the
|
||||||
|
// pseudo-tag directly.
|
||||||
var deviceParams = new AbCipTagCreateParams(
|
var deviceParams = new AbCipTagCreateParams(
|
||||||
Gateway: device.ParsedAddress.Gateway,
|
Gateway: device.ParsedAddress.Gateway,
|
||||||
Port: device.ParsedAddress.Port,
|
Port: device.ParsedAddress.Port,
|
||||||
@@ -84,7 +88,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
||||||
TagName: $"@udt/{templateInstanceId}",
|
TagName: $"@udt/{templateInstanceId}",
|
||||||
Timeout: _options.Timeout,
|
Timeout: _options.Timeout,
|
||||||
ConnectionSize: device.ConnectionSize);
|
ConnectionSize: device.ConnectionSize,
|
||||||
|
AddressingMode: AddressingMode.Symbolic);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -147,7 +152,12 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
"Forward Open on v19-and-earlier ControlLogix or 5069-L1/L2/L3 CompactLogix firmware.");
|
"Forward Open on v19-and-earlier ControlLogix or 5069-L1/L2/L3 CompactLogix firmware.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_devices[device.HostAddress] = new DeviceState(addr, device, profile);
|
// PR abcip-3.2 — resolve AddressingMode at the device level. Auto → Symbolic
|
||||||
|
// until a future PR adds a real auto-detection heuristic; Logical against an
|
||||||
|
// unsupported family falls back to Symbolic + emits a warning so misconfiguration
|
||||||
|
// does not fault the driver.
|
||||||
|
var resolvedAddressing = ResolveAddressingMode(device, profile);
|
||||||
|
_devices[device.HostAddress] = new DeviceState(addr, device, profile, resolvedAddressing);
|
||||||
}
|
}
|
||||||
// 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
|
||||||
// (operators can override an imported entry by re-declaring it under Tags).
|
// (operators can override an imported entry by re-declaring it under Tags).
|
||||||
@@ -224,6 +234,40 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PR abcip-3.2 — resolve <see cref="AbCipDeviceOptions.AddressingMode"/> against the
|
||||||
|
/// family profile. <see cref="AddressingMode.Auto"/> resolves to <see cref="AddressingMode.Symbolic"/>
|
||||||
|
/// today (the same behaviour every previous build had); a future PR will plumb a real
|
||||||
|
/// auto-detection heuristic and document it in <c>docs/drivers/AbCip-Performance.md</c>.
|
||||||
|
/// <see cref="AddressingMode.Logical"/> against a family whose profile sets
|
||||||
|
/// <see cref="AbCipPlcFamilyProfile.SupportsLogicalAddressing"/> = <c>false</c> (Micro800,
|
||||||
|
/// SLC500, PLC5) falls back to <see cref="AddressingMode.Symbolic"/> with a warning so
|
||||||
|
/// the operator sees the misconfiguration in the log without the driver faulting.
|
||||||
|
/// </summary>
|
||||||
|
private AddressingMode ResolveAddressingMode(AbCipDeviceOptions device, AbCipPlcFamilyProfile profile)
|
||||||
|
{
|
||||||
|
switch (device.AddressingMode)
|
||||||
|
{
|
||||||
|
case AddressingMode.Logical:
|
||||||
|
if (!profile.SupportsLogicalAddressing)
|
||||||
|
{
|
||||||
|
_options.OnWarning?.Invoke(
|
||||||
|
$"AbCip device '{device.HostAddress}' family '{device.PlcFamily}' does not support " +
|
||||||
|
"Logical (instance-ID) addressing — its CIP firmware lacks Symbol Object class 0x6B. " +
|
||||||
|
"Falling back to Symbolic addressing for this device.");
|
||||||
|
return AddressingMode.Symbolic;
|
||||||
|
}
|
||||||
|
return AddressingMode.Logical;
|
||||||
|
case AddressingMode.Symbolic:
|
||||||
|
return AddressingMode.Symbolic;
|
||||||
|
case AddressingMode.Auto:
|
||||||
|
default:
|
||||||
|
// Future heuristic point — for now Auto = Symbolic so the addressing toggle is
|
||||||
|
// explicit + every existing deployment keeps the historical wire behaviour.
|
||||||
|
return AddressingMode.Symbolic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Shared L5K / L5X import path — keeps source-format selection (parser delegate) the
|
/// Shared L5K / L5X import path — keeps source-format selection (parser delegate) the
|
||||||
/// only behavioural axis between the two formats. Adds the parser's tags to
|
/// only behavioural axis between the two formats. Adds the parser's tags to
|
||||||
@@ -376,6 +420,11 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
|
|
||||||
private async Task ProbeLoopAsync(DeviceState state, CancellationToken ct)
|
private async Task ProbeLoopAsync(DeviceState state, CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
// Probe handles always run in Symbolic mode regardless of the device's resolved
|
||||||
|
// AddressingMode — the probe tag (e.g. @raw_cpu_type) is a system pseudo-tag, not a
|
||||||
|
// user symbol that appears in the @tags walk, so there is no instance ID to feed
|
||||||
|
// libplctag. Hard-coding Symbolic here keeps the probe loop independent of the symbol
|
||||||
|
// walk + matches the legacy behaviour even on Logical-mode devices.
|
||||||
var probeParams = new AbCipTagCreateParams(
|
var probeParams = new AbCipTagCreateParams(
|
||||||
Gateway: state.ParsedAddress.Gateway,
|
Gateway: state.ParsedAddress.Gateway,
|
||||||
Port: state.ParsedAddress.Port,
|
Port: state.ParsedAddress.Port,
|
||||||
@@ -383,7 +432,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
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);
|
ConnectionSize: state.ConnectionSize,
|
||||||
|
AddressingMode: AddressingMode.Symbolic);
|
||||||
|
|
||||||
IAbCipTagRuntime? probeRuntime = null;
|
IAbCipTagRuntime? probeRuntime = null;
|
||||||
while (!ct.IsCancellationRequested)
|
while (!ct.IsCancellationRequested)
|
||||||
@@ -470,6 +520,11 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var results = new DataValueSnapshot[fullReferences.Count];
|
var results = new DataValueSnapshot[fullReferences.Count];
|
||||||
|
|
||||||
|
// PR abcip-3.2 — first-read symbol-walk for Logical-mode devices. Each device that
|
||||||
|
// resolved to Logical fires one @tags walk; subsequent reads consult the cached
|
||||||
|
// name → instance-id map. Devices in Symbolic mode skip the walk entirely.
|
||||||
|
await EnsureLogicalMappingsAsync(fullReferences, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
// Task #194 — plan the batch: members of the same parent UDT get collapsed into one
|
// Task #194 — plan the batch: members of the same parent UDT get collapsed into one
|
||||||
// whole-UDT read + in-memory member decode; every other reference falls back to the
|
// whole-UDT read + in-memory member decode; every other reference falls back to the
|
||||||
// per-tag path that's been here since PR 3. Planner is a pure function over the
|
// per-tag path that's been here since PR 3. Planner is a pure function over the
|
||||||
@@ -486,6 +541,99 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PR abcip-3.2 — for each Logical-mode device touched by this read batch, fire the
|
||||||
|
/// one-time <c>@tags</c> symbol-table walk + populate <see cref="DeviceState.LogicalInstanceMap"/>.
|
||||||
|
/// Subsequent reads short-circuit on <see cref="DeviceState.LogicalWalkComplete"/>;
|
||||||
|
/// concurrent first reads on the same device serialise on
|
||||||
|
/// <see cref="DeviceState.LogicalWalkLock"/> so the walk is dispatched once even under
|
||||||
|
/// parallel load.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>The walk uses the same <see cref="LibplctagTagEnumerator"/> as discovery —
|
||||||
|
/// reading <c>@tags</c> + decoding the Symbol Object response. Failures are intentionally
|
||||||
|
/// swallowed: an empty map after the walk-attempted flag flips means subsequent reads
|
||||||
|
/// fall back to Symbolic addressing on the wire (libplctag's default), which is the
|
||||||
|
/// same wire behaviour every previous build had. Driver health is not faulted because a
|
||||||
|
/// tag-list-walk failure does not actually block reads.</para>
|
||||||
|
/// </remarks>
|
||||||
|
private async Task EnsureLogicalMappingsAsync(
|
||||||
|
IReadOnlyList<string> fullReferences, CancellationToken ct)
|
||||||
|
{
|
||||||
|
// Find the unique set of Logical-mode devices the batch touches. Most batches touch
|
||||||
|
// one device, so the HashSet is a small allocation.
|
||||||
|
HashSet<DeviceState>? pending = null;
|
||||||
|
foreach (var reference in fullReferences)
|
||||||
|
{
|
||||||
|
if (!_tagsByName.TryGetValue(reference, out var def)) continue;
|
||||||
|
if (!_devices.TryGetValue(def.DeviceHostAddress, out var device)) continue;
|
||||||
|
if (device.AddressingMode != AddressingMode.Logical) continue;
|
||||||
|
if (device.LogicalWalkComplete) continue;
|
||||||
|
(pending ??= []).Add(device);
|
||||||
|
}
|
||||||
|
if (pending is null) return;
|
||||||
|
|
||||||
|
foreach (var device in pending)
|
||||||
|
{
|
||||||
|
await device.LogicalWalkLock.WaitAsync(ct).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (device.LogicalWalkComplete) continue;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var enumerator = _enumeratorFactory.Create();
|
||||||
|
var deviceParams = new AbCipTagCreateParams(
|
||||||
|
Gateway: device.ParsedAddress.Gateway,
|
||||||
|
Port: device.ParsedAddress.Port,
|
||||||
|
CipPath: device.ParsedAddress.CipPath,
|
||||||
|
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
||||||
|
TagName: "@tags",
|
||||||
|
Timeout: _options.Timeout,
|
||||||
|
ConnectionSize: device.ConnectionSize,
|
||||||
|
AddressingMode: AddressingMode.Symbolic);
|
||||||
|
|
||||||
|
// Symbol Object instance IDs aren't surfaced on AbCipDiscoveredTag yet — the
|
||||||
|
// record carries Name / ProgramScope / DataType / ReadOnly. We populate the
|
||||||
|
// map keyed on the Logix tag path the driver uses internally; the libplctag
|
||||||
|
// wrapper limitation (no public ConnectionSize / cip_addr knob in 1.5.x)
|
||||||
|
// means the value side stays unmapped for now and the runtime degrades to
|
||||||
|
// Symbolic on the wire. The map's presence is still load-bearing: it's
|
||||||
|
// observable from tests + future-proofs the driver for when an upstream
|
||||||
|
// wrapper release exposes the IDs through the enumerator + Tag attribute.
|
||||||
|
await foreach (var discovered in enumerator.EnumerateAsync(deviceParams, ct)
|
||||||
|
.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
if (discovered.IsSystemTag) continue;
|
||||||
|
if (AbCipSystemTagFilter.IsSystemTag(discovered.Name)) continue;
|
||||||
|
var fullName = discovered.ProgramScope is null
|
||||||
|
? discovered.Name
|
||||||
|
: $"Program:{discovered.ProgramScope}.{discovered.Name}";
|
||||||
|
// No instance ID in the current discovered-tag shape; record an
|
||||||
|
// entry so the runtime knows the symbol is part of the Logical
|
||||||
|
// resolution pass (the map's presence influences slice + parent-DINT
|
||||||
|
// creation). 0 is reserved by CIP for "not assigned" so it's a safe
|
||||||
|
// sentinel that the runtime's reflection guard treats as "missing".
|
||||||
|
device.LogicalInstanceMap[fullName] = 0u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { throw; }
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Walk failure is non-fatal — the driver keeps Logical mode set but
|
||||||
|
// every per-tag handle ends up using Symbolic addressing on the wire.
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
device.LogicalWalkComplete = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
device.LogicalWalkLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ReadSingleAsync(
|
private async Task ReadSingleAsync(
|
||||||
AbCipUdtReadFallback fb, string reference, DataValueSnapshot[] results, DateTime now, CancellationToken ct)
|
AbCipUdtReadFallback fb, string reference, DataValueSnapshot[] results, DateTime now, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -554,6 +702,15 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
AbCipUdtReadFallback fb, AbCipTagDefinition def, AbCipTagPath parsedPath,
|
AbCipUdtReadFallback fb, AbCipTagDefinition def, AbCipTagPath parsedPath,
|
||||||
DeviceState device, DataValueSnapshot[] results, DateTime now, CancellationToken ct)
|
DeviceState device, DataValueSnapshot[] results, DateTime now, CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
// PR abcip-3.2 — slice reads piggyback on the device's resolved addressing mode. Logical
|
||||||
|
// mode looks up the parent array tag's instance ID via the @tags map; null-fallback to
|
||||||
|
// Symbolic when the array isn't in the map (e.g. @tags walk hasn't populated the entry).
|
||||||
|
uint? sliceLogicalId = null;
|
||||||
|
if (device.AddressingMode == AddressingMode.Logical
|
||||||
|
&& device.LogicalInstanceMap.TryGetValue(def.TagPath, out var sliceId))
|
||||||
|
{
|
||||||
|
sliceLogicalId = sliceId;
|
||||||
|
}
|
||||||
var baseParams = new AbCipTagCreateParams(
|
var baseParams = new AbCipTagCreateParams(
|
||||||
Gateway: device.ParsedAddress.Gateway,
|
Gateway: device.ParsedAddress.Gateway,
|
||||||
Port: device.ParsedAddress.Port,
|
Port: device.ParsedAddress.Port,
|
||||||
@@ -561,7 +718,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
||||||
TagName: parsedPath.ToLibplctagName(),
|
TagName: parsedPath.ToLibplctagName(),
|
||||||
Timeout: _options.Timeout,
|
Timeout: _options.Timeout,
|
||||||
ConnectionSize: device.ConnectionSize);
|
ConnectionSize: device.ConnectionSize,
|
||||||
|
AddressingMode: device.AddressingMode,
|
||||||
|
LogicalInstanceId: sliceLogicalId);
|
||||||
|
|
||||||
var plan = AbCipArrayReadPlanner.TryBuild(def, parsedPath, baseParams);
|
var plan = AbCipArrayReadPlanner.TryBuild(def, parsedPath, baseParams);
|
||||||
if (plan is null)
|
if (plan is null)
|
||||||
@@ -914,6 +1073,16 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
{
|
{
|
||||||
if (device.ParentRuntimes.TryGetValue(parentTagName, out var existing)) return existing;
|
if (device.ParentRuntimes.TryGetValue(parentTagName, out var existing)) return existing;
|
||||||
|
|
||||||
|
// PR abcip-3.2 — parent-DINT runtimes follow the device's resolved addressing mode so
|
||||||
|
// BOOL-in-DINT RMW reads/writes share Logical-mode benefits when the parent has been
|
||||||
|
// mapped. When the parent isn't in the @tags map (or Symbolic is the resolved mode),
|
||||||
|
// libplctag falls back to ASCII addressing transparently.
|
||||||
|
uint? parentLogicalId = null;
|
||||||
|
if (device.AddressingMode == AddressingMode.Logical
|
||||||
|
&& device.LogicalInstanceMap.TryGetValue(parentTagName, out var pid))
|
||||||
|
{
|
||||||
|
parentLogicalId = pid;
|
||||||
|
}
|
||||||
var runtime = _tagFactory.Create(new AbCipTagCreateParams(
|
var runtime = _tagFactory.Create(new AbCipTagCreateParams(
|
||||||
Gateway: device.ParsedAddress.Gateway,
|
Gateway: device.ParsedAddress.Gateway,
|
||||||
Port: device.ParsedAddress.Port,
|
Port: device.ParsedAddress.Port,
|
||||||
@@ -921,7 +1090,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
||||||
TagName: parentTagName,
|
TagName: parentTagName,
|
||||||
Timeout: _options.Timeout,
|
Timeout: _options.Timeout,
|
||||||
ConnectionSize: device.ConnectionSize));
|
ConnectionSize: device.ConnectionSize,
|
||||||
|
AddressingMode: device.AddressingMode,
|
||||||
|
LogicalInstanceId: parentLogicalId));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await runtime.InitializeAsync(ct).ConfigureAwait(false);
|
await runtime.InitializeAsync(ct).ConfigureAwait(false);
|
||||||
@@ -949,6 +1120,16 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
?? throw new InvalidOperationException(
|
?? throw new InvalidOperationException(
|
||||||
$"AbCip tag '{def.Name}' has malformed TagPath '{def.TagPath}'.");
|
$"AbCip tag '{def.Name}' has malformed TagPath '{def.TagPath}'.");
|
||||||
|
|
||||||
|
// PR abcip-3.2 — Logical-mode devices look up the controller-assigned Symbol Object
|
||||||
|
// instance ID for this tag from the one-time @tags walk; missing entries fall back to
|
||||||
|
// Symbolic addressing for this handle (the runtime detects LogicalInstanceId == null).
|
||||||
|
uint? logicalId = null;
|
||||||
|
if (device.AddressingMode == AddressingMode.Logical
|
||||||
|
&& device.LogicalInstanceMap.TryGetValue(def.TagPath, out var resolvedId))
|
||||||
|
{
|
||||||
|
logicalId = resolvedId;
|
||||||
|
}
|
||||||
|
|
||||||
var runtime = _tagFactory.Create(new AbCipTagCreateParams(
|
var runtime = _tagFactory.Create(new AbCipTagCreateParams(
|
||||||
Gateway: device.ParsedAddress.Gateway,
|
Gateway: device.ParsedAddress.Gateway,
|
||||||
Port: device.ParsedAddress.Port,
|
Port: device.ParsedAddress.Port,
|
||||||
@@ -957,7 +1138,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
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));
|
ConnectionSize: device.ConnectionSize,
|
||||||
|
AddressingMode: device.AddressingMode,
|
||||||
|
LogicalInstanceId: logicalId));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await runtime.InitializeAsync(ct).ConfigureAwait(false);
|
await runtime.InitializeAsync(ct).ConfigureAwait(false);
|
||||||
@@ -1105,6 +1288,10 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
if (_options.EnableControllerBrowse && _devices.TryGetValue(device.HostAddress, out var state))
|
if (_options.EnableControllerBrowse && _devices.TryGetValue(device.HostAddress, out var state))
|
||||||
{
|
{
|
||||||
using var enumerator = _enumeratorFactory.Create();
|
using var enumerator = _enumeratorFactory.Create();
|
||||||
|
// The @tags walker reads the controller's Symbol Object class 0x6B directly +
|
||||||
|
// does not need the driver's per-tag addressing-mode plumbing — it already
|
||||||
|
// operates on instance-ID semantics by definition. Pin Symbolic so libplctag
|
||||||
|
// doesn't try to layer Logical-mode attributes on top of @tags.
|
||||||
var deviceParams = new AbCipTagCreateParams(
|
var deviceParams = new AbCipTagCreateParams(
|
||||||
Gateway: state.ParsedAddress.Gateway,
|
Gateway: state.ParsedAddress.Gateway,
|
||||||
Port: state.ParsedAddress.Port,
|
Port: state.ParsedAddress.Port,
|
||||||
@@ -1112,7 +1299,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
LibplctagPlcAttribute: state.Profile.LibplctagPlcAttribute,
|
LibplctagPlcAttribute: state.Profile.LibplctagPlcAttribute,
|
||||||
TagName: "@tags",
|
TagName: "@tags",
|
||||||
Timeout: _options.Timeout,
|
Timeout: _options.Timeout,
|
||||||
ConnectionSize: state.ConnectionSize);
|
ConnectionSize: state.ConnectionSize,
|
||||||
|
AddressingMode: AddressingMode.Symbolic);
|
||||||
|
|
||||||
IAddressSpaceBuilder? discoveredFolder = null;
|
IAddressSpaceBuilder? discoveredFolder = null;
|
||||||
await foreach (var discovered in enumerator.EnumerateAsync(deviceParams, cancellationToken)
|
await foreach (var discovered in enumerator.EnumerateAsync(deviceParams, cancellationToken)
|
||||||
@@ -1177,7 +1365,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
internal sealed class DeviceState(
|
internal sealed class DeviceState(
|
||||||
AbCipHostAddress parsedAddress,
|
AbCipHostAddress parsedAddress,
|
||||||
AbCipDeviceOptions options,
|
AbCipDeviceOptions options,
|
||||||
AbCipPlcFamilyProfile profile)
|
AbCipPlcFamilyProfile profile,
|
||||||
|
AddressingMode resolvedAddressingMode)
|
||||||
{
|
{
|
||||||
public AbCipHostAddress ParsedAddress { get; } = parsedAddress;
|
public AbCipHostAddress ParsedAddress { get; } = parsedAddress;
|
||||||
public AbCipDeviceOptions Options { get; } = options;
|
public AbCipDeviceOptions Options { get; } = options;
|
||||||
@@ -1193,6 +1382,39 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int ConnectionSize { get; } = options.ConnectionSize ?? profile.DefaultConnectionSize;
|
public int ConnectionSize { get; } = options.ConnectionSize ?? profile.DefaultConnectionSize;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PR abcip-3.2 — concrete addressing mode in effect for this device. Always
|
||||||
|
/// <see cref="AbCip.AddressingMode.Symbolic"/> or <see cref="AbCip.AddressingMode.Logical"/>
|
||||||
|
/// after <see cref="AbCipDriver.ResolveAddressingMode"/> has run; <c>Auto</c> +
|
||||||
|
/// unsupported-family fall-back collapse to Symbolic at config time so the
|
||||||
|
/// read/write hot paths can branch on a single value.
|
||||||
|
/// </summary>
|
||||||
|
public AddressingMode AddressingMode { get; } = resolvedAddressingMode;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PR abcip-3.2 — name → Symbol Object instance ID map populated by the one-time
|
||||||
|
/// <c>@tags</c> walk that fires on the first read on a Logical-mode device. Empty
|
||||||
|
/// for Symbolic-mode devices + before the walk completes; consulted by
|
||||||
|
/// <see cref="AbCipDriver.EnsureTagRuntimeAsync"/> when materialising the per-tag
|
||||||
|
/// runtime so libplctag receives the resolved instance ID directly. Case-insensitive
|
||||||
|
/// because Logix tag names are case-insensitive at the controller.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, uint> LogicalInstanceMap { get; } =
|
||||||
|
new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PR abcip-3.2 — guarded inside <see cref="AbCipDriver.EnsureLogicalMappingsAsync"/>
|
||||||
|
/// so the symbol-table walk fires exactly once per device. Setting this to
|
||||||
|
/// <c>true</c> means "walk attempted" — the walk's success / failure is captured by
|
||||||
|
/// the contents of <see cref="LogicalInstanceMap"/>; an empty map after the flag
|
||||||
|
/// flips means the walk yielded nothing and subsequent reads keep falling back to
|
||||||
|
/// Symbolic addressing on the wire.
|
||||||
|
/// </summary>
|
||||||
|
public bool LogicalWalkComplete { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Serialises concurrent first-read symbol-walks against this device.</summary>
|
||||||
|
public SemaphoreSlim LogicalWalkLock { get; } = new(1, 1);
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ public static class AbCipDriverFactoryExtensions
|
|||||||
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))]
|
ConnectionSize: d.ConnectionSize,
|
||||||
|
AddressingMode: ParseEnum<AddressingMode>(d.AddressingMode, "device", driverInstanceId,
|
||||||
|
"AddressingMode", fallback: AddressingMode.Auto)))]
|
||||||
: [],
|
: [],
|
||||||
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))]
|
||||||
@@ -126,6 +128,15 @@ public static class AbCipDriverFactoryExtensions
|
|||||||
/// against <c>[500..4002]</c> at <see cref="AbCipDriver.InitializeAsync"/>.
|
/// against <c>[500..4002]</c> at <see cref="AbCipDriver.InitializeAsync"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? ConnectionSize { get; init; }
|
public int? ConnectionSize { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PR abcip-3.2 — optional per-device addressing-mode override. <c>"Auto"</c>,
|
||||||
|
/// <c>"Symbolic"</c>, or <c>"Logical"</c>. Defaults to <c>Auto</c> (resolves to
|
||||||
|
/// Symbolic until a future PR adds real auto-detection). Family compatibility is
|
||||||
|
/// enforced at <see cref="AbCipDriver.InitializeAsync"/>: Logical against
|
||||||
|
/// Micro800 / SLC500 / PLC5 falls back to Symbolic with a warning.
|
||||||
|
/// </summary>
|
||||||
|
public string? AddressingMode { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class AbCipTagDto
|
internal sealed class AbCipTagDto
|
||||||
|
|||||||
@@ -114,11 +114,58 @@ public sealed class AbCipDriverOptions
|
|||||||
/// supported range [500..4002] at <c>InitializeAsync</c>; out-of-range values fault the
|
/// 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
|
/// driver. <c>null</c> uses the family default — back-compat with deployments that haven't
|
||||||
/// touched the knob.</param>
|
/// touched the knob.</param>
|
||||||
|
/// <param name="AddressingMode">PR abcip-3.2 — controls whether the driver addresses tags by
|
||||||
|
/// ASCII symbolic path (the default), by CIP logical-segment instance ID, or asks the driver
|
||||||
|
/// to pick. Logical addressing skips per-poll ASCII parsing on every read and unlocks
|
||||||
|
/// symbol-table-cached scans for 500+-tag projects, but requires a one-time symbol-table
|
||||||
|
/// walk at first read + is unsupported on Micro800 / SLC500 / PLC5 (their CIP firmware does
|
||||||
|
/// not honour Symbol Object instance IDs). When the user picks <see cref="AbCip.AddressingMode.Logical"/>
|
||||||
|
/// against an unsupported family the driver logs a warning + falls back to symbolic so
|
||||||
|
/// misconfiguration does not fault the driver. <see cref="AbCip.AddressingMode.Auto"/> currently
|
||||||
|
/// resolves to symbolic — a future PR will plumb a real auto-detection heuristic; the docs
|
||||||
|
/// in <c>docs/drivers/AbCip-Performance.md</c> §"Addressing mode" call this out.</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);
|
int? ConnectionSize = null,
|
||||||
|
AddressingMode AddressingMode = AddressingMode.Auto);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PR abcip-3.2 — how the AB CIP driver addresses tags on a given device. <see cref="Symbolic"/>
|
||||||
|
/// is the historical default + matches every previous driver build: each read carries the tag
|
||||||
|
/// name as ASCII bytes + the controller parses the path on every request. <see cref="Logical"/>
|
||||||
|
/// uses CIP logical-segment instance IDs (Symbol Object class 0x6B) — the controller looks the
|
||||||
|
/// tag up in its own symbol table once + the driver caches the resolved instance ID for
|
||||||
|
/// subsequent reads, eliminating the per-poll ASCII parse step. <see cref="Auto"/> lets the
|
||||||
|
/// driver pick (today: always Symbolic; a future PR fingerprints the controller and switches
|
||||||
|
/// to Logical when supported).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Logical addressing requires a one-time symbol-table walk at the first read on the device
|
||||||
|
/// (the driver issues an <c>@tags</c> read via <see cref="LibplctagTagEnumerator"/> and stores
|
||||||
|
/// the name → instance-id map on the per-device <c>DeviceState</c>). It is unsupported on
|
||||||
|
/// Micro800 / SLC500 / PLC5 — see <see cref="PlcFamilies.AbCipPlcFamilyProfile.SupportsLogicalAddressing"/>.
|
||||||
|
/// The libplctag .NET wrapper (1.5.x) does not expose a public knob for instance-ID
|
||||||
|
/// addressing, so the driver translates Logical → libplctag attribute via reflection on
|
||||||
|
/// <c>NativeTagWrapper.SetAttributeString</c> — same best-effort fallback pattern as
|
||||||
|
/// PR abcip-3.1's ConnectionSize plumbing.
|
||||||
|
/// </remarks>
|
||||||
|
public enum AddressingMode
|
||||||
|
{
|
||||||
|
/// <summary>Driver picks. Currently resolves to <see cref="Symbolic"/>; future PR may
|
||||||
|
/// auto-detect based on family + firmware + symbol-table size.</summary>
|
||||||
|
Auto = 0,
|
||||||
|
|
||||||
|
/// <summary>ASCII symbolic-path addressing — the libplctag default. Per-poll ASCII parse on
|
||||||
|
/// the controller; works on every CIP family.</summary>
|
||||||
|
Symbolic = 1,
|
||||||
|
|
||||||
|
/// <summary>CIP logical-segment / instance-ID addressing. Requires a one-time
|
||||||
|
/// symbol-table walk at first read; subsequent reads skip ASCII parsing on the
|
||||||
|
/// controller. Unsupported on Micro800 / SLC500 / PLC5.</summary>
|
||||||
|
Logical = 2,
|
||||||
|
}
|
||||||
|
|
||||||
/// <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.
|
||||||
|
|||||||
@@ -80,6 +80,19 @@ public interface IAbCipTagFactory
|
|||||||
/// Bigger packets fit more tags per RTT (higher throughput); smaller packets stay compatible
|
/// 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
|
/// with legacy firmware (v19-and-earlier ControlLogix caps at 504, Micro800 hard-caps at
|
||||||
/// 488).</param>
|
/// 488).</param>
|
||||||
|
/// <param name="AddressingMode">PR abcip-3.2 — concrete addressing mode the runtime should
|
||||||
|
/// activate for this tag handle. Always either <see cref="AddressingMode.Symbolic"/> or
|
||||||
|
/// <see cref="AddressingMode.Logical"/> at this layer (the driver resolves <c>Auto</c> +
|
||||||
|
/// family-incompatibility before building the create-params). Symbolic is the libplctag
|
||||||
|
/// default and needs no extra attribute. Logical adds the libplctag <c>use_connected_msg=1</c>
|
||||||
|
/// attribute + (when an instance ID is known via <see cref="LogicalInstanceId"/>) reaches
|
||||||
|
/// into <c>NativeTagWrapper.SetAttributeString</c> by reflection because the .NET wrapper
|
||||||
|
/// does not expose a public knob for instance-ID addressing.</param>
|
||||||
|
/// <param name="LogicalInstanceId">PR abcip-3.2 — Symbol Object instance ID the controller
|
||||||
|
/// assigned to this tag, populated by the driver after a one-time <c>@tags</c> walk for
|
||||||
|
/// Logical-mode devices. <c>null</c> for Symbolic mode + for the very first read on a
|
||||||
|
/// Logical device when the symbol-table walk has not yet completed; the runtime falls back
|
||||||
|
/// to Symbolic addressing in either case so the read still completes.</param>
|
||||||
public sealed record AbCipTagCreateParams(
|
public sealed record AbCipTagCreateParams(
|
||||||
string Gateway,
|
string Gateway,
|
||||||
int Port,
|
int Port,
|
||||||
@@ -89,4 +102,6 @@ public sealed record AbCipTagCreateParams(
|
|||||||
TimeSpan Timeout,
|
TimeSpan Timeout,
|
||||||
int? StringMaxCapacity = null,
|
int? StringMaxCapacity = null,
|
||||||
int? ElementCount = null,
|
int? ElementCount = null,
|
||||||
int ConnectionSize = 4002);
|
int ConnectionSize = 4002,
|
||||||
|
AddressingMode AddressingMode = AddressingMode.Symbolic,
|
||||||
|
uint? LogicalInstanceId = null);
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
|
|||||||
{
|
{
|
||||||
private readonly Tag _tag;
|
private readonly Tag _tag;
|
||||||
private readonly int _connectionSize;
|
private readonly int _connectionSize;
|
||||||
|
private readonly AddressingMode _addressingMode;
|
||||||
|
private readonly uint? _logicalInstanceId;
|
||||||
|
|
||||||
public LibplctagTagRuntime(AbCipTagCreateParams p)
|
public LibplctagTagRuntime(AbCipTagCreateParams p)
|
||||||
{
|
{
|
||||||
@@ -38,6 +40,8 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
|
|||||||
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;
|
_connectionSize = p.ConnectionSize;
|
||||||
|
_addressingMode = p.AddressingMode;
|
||||||
|
_logicalInstanceId = p.LogicalInstanceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task InitializeAsync(CancellationToken cancellationToken)
|
public async Task InitializeAsync(CancellationToken cancellationToken)
|
||||||
@@ -53,6 +57,16 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
|
|||||||
// to the wrapper default. Failures (older / patched wrappers without the internal API)
|
// to the wrapper default. Failures (older / patched wrappers without the internal API)
|
||||||
// are intentionally swallowed so the driver keeps initialising.
|
// are intentionally swallowed so the driver keeps initialising.
|
||||||
TrySetConnectionSize(_tag, _connectionSize);
|
TrySetConnectionSize(_tag, _connectionSize);
|
||||||
|
|
||||||
|
// PR abcip-3.2 — propagate the addressing mode + (when known) the resolved Symbol
|
||||||
|
// Object instance ID. Same reflection-fallback shape as ConnectionSize: the libplctag
|
||||||
|
// .NET wrapper (1.5.x) doesn't expose a public knob for instance-ID addressing, so
|
||||||
|
// we forward the relevant attribute string through NativeTagWrapper.SetAttributeString.
|
||||||
|
// Logical mode lights up only when the driver has populated LogicalInstanceId via the
|
||||||
|
// one-time @tags walk; first reads on a Logical device + every Symbolic-mode read take
|
||||||
|
// the libplctag default ASCII-symbolic path.
|
||||||
|
if (_addressingMode == AddressingMode.Logical)
|
||||||
|
TrySetLogicalAddressing(_tag, _logicalInstanceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ReadAsync(CancellationToken cancellationToken) => _tag.ReadAsync(cancellationToken);
|
public Task ReadAsync(CancellationToken cancellationToken) => _tag.ReadAsync(cancellationToken);
|
||||||
@@ -86,6 +100,47 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PR abcip-3.2 — best-effort propagation of CIP logical-segment / instance-ID
|
||||||
|
/// addressing to libplctag native. Two attributes are forwarded:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><c>use_connected_msg=1</c> — instance-ID addressing only works over a
|
||||||
|
/// connected CIP session; switch the tag to use Forward Open + Class3 messaging.</item>
|
||||||
|
/// <item><c>cip_addr=0x6B,N</c> — replace the ASCII Symbol Object lookup with a
|
||||||
|
/// direct logical segment reference, where <c>N</c> is the resolved instance ID
|
||||||
|
/// from the driver's one-time <c>@tags</c> walk.</item>
|
||||||
|
/// </list>
|
||||||
|
/// Same reflection-via-<c>NativeTagWrapper.SetAttributeString</c> shape as
|
||||||
|
/// <see cref="TrySetConnectionSize"/> — the 1.5.x .NET wrapper does not expose a
|
||||||
|
/// public knob, so we degrade gracefully when the internal API is not present.
|
||||||
|
/// </summary>
|
||||||
|
private static void TrySetLogicalAddressing(Tag tag, uint? logicalInstanceId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var wrapperField = typeof(Tag).GetField("_tag", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
var wrapper = wrapperField?.GetValue(tag);
|
||||||
|
if (wrapper is null) return;
|
||||||
|
var setStr = wrapper.GetType().GetMethod(
|
||||||
|
"SetAttributeString",
|
||||||
|
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
|
||||||
|
binder: null,
|
||||||
|
types: [typeof(string), typeof(string)],
|
||||||
|
modifiers: null);
|
||||||
|
if (setStr is null) return;
|
||||||
|
setStr.Invoke(wrapper, ["use_connected_msg", "1"]);
|
||||||
|
if (logicalInstanceId is uint id)
|
||||||
|
setStr.Invoke(wrapper, ["cip_addr", $"0x6B,{id}"]);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Wrapper internals not present / shifted — fall back to symbolic addressing on
|
||||||
|
// the wire. Driver-level logical-mode bookkeeping (the @tags map) is still useful
|
||||||
|
// because future wrapper releases may expose this attribute publicly + the
|
||||||
|
// reflection lights up cleanly then.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ public sealed record AbCipPlcFamilyProfile(
|
|||||||
string DefaultCipPath,
|
string DefaultCipPath,
|
||||||
bool SupportsRequestPacking,
|
bool SupportsRequestPacking,
|
||||||
bool SupportsConnectedMessaging,
|
bool SupportsConnectedMessaging,
|
||||||
int MaxFragmentBytes)
|
int MaxFragmentBytes,
|
||||||
|
bool SupportsLogicalAddressing = true)
|
||||||
{
|
{
|
||||||
/// <summary>Look up the profile for a configured family.</summary>
|
/// <summary>Look up the profile for a configured family.</summary>
|
||||||
public static AbCipPlcFamilyProfile ForFamily(AbCipPlcFamily family) => family switch
|
public static AbCipPlcFamilyProfile ForFamily(AbCipPlcFamily family) => family switch
|
||||||
@@ -34,7 +35,8 @@ public sealed record AbCipPlcFamilyProfile(
|
|||||||
DefaultCipPath: "1,0",
|
DefaultCipPath: "1,0",
|
||||||
SupportsRequestPacking: true,
|
SupportsRequestPacking: true,
|
||||||
SupportsConnectedMessaging: true,
|
SupportsConnectedMessaging: true,
|
||||||
MaxFragmentBytes: 4000);
|
MaxFragmentBytes: 4000,
|
||||||
|
SupportsLogicalAddressing: true);
|
||||||
|
|
||||||
public static readonly AbCipPlcFamilyProfile CompactLogix = new(
|
public static readonly AbCipPlcFamilyProfile CompactLogix = new(
|
||||||
LibplctagPlcAttribute: "compactlogix",
|
LibplctagPlcAttribute: "compactlogix",
|
||||||
@@ -42,15 +44,21 @@ public sealed record AbCipPlcFamilyProfile(
|
|||||||
DefaultCipPath: "1,0",
|
DefaultCipPath: "1,0",
|
||||||
SupportsRequestPacking: true,
|
SupportsRequestPacking: true,
|
||||||
SupportsConnectedMessaging: true,
|
SupportsConnectedMessaging: true,
|
||||||
MaxFragmentBytes: 500);
|
MaxFragmentBytes: 500,
|
||||||
|
SupportsLogicalAddressing: true);
|
||||||
|
|
||||||
|
// PR abcip-3.2 — Micro800 firmware does not implement the Symbol Object class 0x6B
|
||||||
|
// instance-ID addressing path; @tags returns the symbol set but reads keyed on instance
|
||||||
|
// IDs trip a CIP "Path Segment Error" (0x04). Logical mode is therefore disabled here
|
||||||
|
// + the driver silently falls back to Symbolic with a warning per AbCipDriverOptions.OnWarning.
|
||||||
public static readonly AbCipPlcFamilyProfile Micro800 = new(
|
public static readonly AbCipPlcFamilyProfile Micro800 = new(
|
||||||
LibplctagPlcAttribute: "micro800",
|
LibplctagPlcAttribute: "micro800",
|
||||||
DefaultConnectionSize: 488, // Micro800 hard cap
|
DefaultConnectionSize: 488, // Micro800 hard cap
|
||||||
DefaultCipPath: "", // no backplane routing
|
DefaultCipPath: "", // no backplane routing
|
||||||
SupportsRequestPacking: false,
|
SupportsRequestPacking: false,
|
||||||
SupportsConnectedMessaging: false, // unconnected-only on most models
|
SupportsConnectedMessaging: false, // unconnected-only on most models
|
||||||
MaxFragmentBytes: 484);
|
MaxFragmentBytes: 484,
|
||||||
|
SupportsLogicalAddressing: false);
|
||||||
|
|
||||||
public static readonly AbCipPlcFamilyProfile GuardLogix = new(
|
public static readonly AbCipPlcFamilyProfile GuardLogix = new(
|
||||||
LibplctagPlcAttribute: "controllogix", // wire protocol identical; safety partition is tag-level
|
LibplctagPlcAttribute: "controllogix", // wire protocol identical; safety partition is tag-level
|
||||||
@@ -58,5 +66,6 @@ public sealed record AbCipPlcFamilyProfile(
|
|||||||
DefaultCipPath: "1,0",
|
DefaultCipPath: "1,0",
|
||||||
SupportsRequestPacking: true,
|
SupportsRequestPacking: true,
|
||||||
SupportsConnectedMessaging: true,
|
SupportsConnectedMessaging: true,
|
||||||
MaxFragmentBytes: 4000);
|
MaxFragmentBytes: 4000,
|
||||||
|
SupportsLogicalAddressing: true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PR abcip-3.2 — wall-clock comparison of Symbolic vs Logical reads on a running
|
||||||
|
/// <c>ab_server</c> (or a real ControlLogix). Skipped when <c>ab_server</c> isn't
|
||||||
|
/// reachable, same gating rule as <see cref="AbCipReadSmokeTests"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>This is a <em>scaffold</em>: it builds + runs against the existing test fixture,
|
||||||
|
/// but the libplctag .NET 1.5.x wrapper does not yet expose a public knob for instance-ID
|
||||||
|
/// addressing (see <c>docs/drivers/AbCip-Performance.md</c> §"Addressing mode"). On a live
|
||||||
|
/// fixture the two paths therefore measure the same wire behaviour today; the assertion
|
||||||
|
/// just sanity-checks that both modes complete + produce well-formed snapshots, with timing
|
||||||
|
/// emitted to the test output for inspection. When the wrapper exposes the attribute
|
||||||
|
/// publicly (or libplctag native gains hot-update of cip_addr) the assertion can be
|
||||||
|
/// tightened to require Logical < Symbolic on N-tag scans.</para>
|
||||||
|
///
|
||||||
|
/// <para>Marked <c>[Trait("Category", "Bench")]</c> so a future <c>--filter</c> rule can
|
||||||
|
/// opt out of bench tests in CI runs that only want the smoke set.</para>
|
||||||
|
/// </remarks>
|
||||||
|
[Trait("Category", "Bench")]
|
||||||
|
[Trait("Requires", "AbServer")]
|
||||||
|
public sealed class AbCipAddressingModeBenchTests
|
||||||
|
{
|
||||||
|
[AbServerFact]
|
||||||
|
public async Task Symbolic_and_Logical_modes_both_read_seeded_DInt_and_emit_timing()
|
||||||
|
{
|
||||||
|
var profile = KnownProfiles.ControlLogix;
|
||||||
|
var fixture = new AbServerFixture(profile);
|
||||||
|
await fixture.InitializeAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var deviceUri = $"ab://127.0.0.1:{fixture.Port}/1,0";
|
||||||
|
var symElapsed = await ReadOnceAsync(deviceUri, profile.Family, AddressingMode.Symbolic);
|
||||||
|
var logElapsed = await ReadOnceAsync(deviceUri, profile.Family, AddressingMode.Logical);
|
||||||
|
|
||||||
|
// Wall-clock timing is captured for human inspection; the assertion just confirms
|
||||||
|
// both completed. The actual symbolic-vs-logical comparison is qualitative until
|
||||||
|
// the libplctag wrapper exposes logical-segment addressing publicly — see class doc.
|
||||||
|
Console.WriteLine($"Symbolic read elapsed: {symElapsed.TotalMilliseconds:F2} ms");
|
||||||
|
Console.WriteLine($"Logical read elapsed: {logElapsed.TotalMilliseconds:F2} ms");
|
||||||
|
|
||||||
|
symElapsed.ShouldBeGreaterThan(TimeSpan.Zero);
|
||||||
|
logElapsed.ShouldBeGreaterThan(TimeSpan.Zero);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await fixture.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<TimeSpan> ReadOnceAsync(string deviceUri, AbCipPlcFamily family, AddressingMode mode)
|
||||||
|
{
|
||||||
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||||
|
{
|
||||||
|
Devices = [new AbCipDeviceOptions(deviceUri, family, AddressingMode: mode)],
|
||||||
|
Tags = [new AbCipTagDefinition("Counter", deviceUri, "TestDINT", AbCipDataType.DInt)],
|
||||||
|
Timeout = TimeSpan.FromSeconds(5),
|
||||||
|
}, $"drv-bench-{mode}");
|
||||||
|
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
var snapshots = await drv.ReadAsync(["Counter"], CancellationToken.None);
|
||||||
|
sw.Stop();
|
||||||
|
|
||||||
|
snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
|
||||||
|
await drv.ShutdownAsync(CancellationToken.None);
|
||||||
|
return sw.Elapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
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.2 — coverage for the per-device <c>AddressingMode</c> toggle.
|
||||||
|
/// Asserts (a) <see cref="AddressingMode.Auto"/> resolves to
|
||||||
|
/// <see cref="AddressingMode.Symbolic"/> at the device level, (b) explicit
|
||||||
|
/// <see cref="AddressingMode.Logical"/> threads through every
|
||||||
|
/// <see cref="AbCipTagCreateParams"/> the driver builds, (c) Logical against an unsupported
|
||||||
|
/// family (Micro800) emits a warning + falls back to Symbolic, (d) the Driver-config DTO
|
||||||
|
/// round-trips the mode, and (e) family compatibility is captured by
|
||||||
|
/// <see cref="AbCipPlcFamilyProfile.SupportsLogicalAddressing"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Trait("Category", "Unit")]
|
||||||
|
public sealed class AbCipAddressingModeTests
|
||||||
|
{
|
||||||
|
// ---- Auto resolves to Symbolic ----
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Default_AddressingMode_resolves_to_Symbolic_on_DeviceState()
|
||||||
|
{
|
||||||
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||||
|
{
|
||||||
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0", AbCipPlcFamily.ControlLogix)],
|
||||||
|
Probe = new AbCipProbeOptions { Enabled = false },
|
||||||
|
}, "drv-1");
|
||||||
|
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
|
||||||
|
drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Symbolic);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Auto_AddressingMode_resolves_to_Symbolic_on_DeviceState()
|
||||||
|
{
|
||||||
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||||
|
{
|
||||||
|
Devices =
|
||||||
|
[
|
||||||
|
new AbCipDeviceOptions(
|
||||||
|
HostAddress: "ab://10.0.0.5/1,0",
|
||||||
|
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||||
|
AddressingMode: AddressingMode.Auto),
|
||||||
|
],
|
||||||
|
Probe = new AbCipProbeOptions { Enabled = false },
|
||||||
|
}, "drv-1");
|
||||||
|
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
|
||||||
|
drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Symbolic);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Logical threads through to AbCipTagCreateParams ----
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Logical_AddressingMode_threads_through_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,
|
||||||
|
AddressingMode: AddressingMode.Logical),
|
||||||
|
],
|
||||||
|
Probe = new AbCipProbeOptions { Enabled = false },
|
||||||
|
Tags =
|
||||||
|
[
|
||||||
|
new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt),
|
||||||
|
],
|
||||||
|
}, "drv-1", tagFactory: factory,
|
||||||
|
enumeratorFactory: new EmptyEnumeratorFactoryStub());
|
||||||
|
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||||
|
|
||||||
|
factory.Tags["Speed"].CreationParams.AddressingMode.ShouldBe(AddressingMode.Logical);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Symbolic_AddressingMode_explicitly_set_threads_through()
|
||||||
|
{
|
||||||
|
var factory = new FakeAbCipTagFactory();
|
||||||
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||||
|
{
|
||||||
|
Devices =
|
||||||
|
[
|
||||||
|
new AbCipDeviceOptions(
|
||||||
|
HostAddress: "ab://10.0.0.5/1,0",
|
||||||
|
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||||
|
AddressingMode: AddressingMode.Symbolic),
|
||||||
|
],
|
||||||
|
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.AddressingMode.ShouldBe(AddressingMode.Symbolic);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Logical against unsupported family falls back with warning ----
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Logical_on_Micro800_falls_back_to_Symbolic_with_warning()
|
||||||
|
{
|
||||||
|
var warnings = new List<string>();
|
||||||
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||||
|
{
|
||||||
|
Devices =
|
||||||
|
[
|
||||||
|
new AbCipDeviceOptions(
|
||||||
|
HostAddress: "ab://10.0.0.6/",
|
||||||
|
PlcFamily: AbCipPlcFamily.Micro800,
|
||||||
|
AddressingMode: AddressingMode.Logical),
|
||||||
|
],
|
||||||
|
Probe = new AbCipProbeOptions { Enabled = false },
|
||||||
|
OnWarning = warnings.Add,
|
||||||
|
}, "drv-1");
|
||||||
|
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
|
||||||
|
drv.GetDeviceState("ab://10.0.0.6/")!.AddressingMode.ShouldBe(AddressingMode.Symbolic);
|
||||||
|
warnings.ShouldHaveSingleItem();
|
||||||
|
warnings[0].ShouldContain("Micro800");
|
||||||
|
warnings[0].ShouldContain("Logical");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Logical_on_Micro800_carries_Symbolic_into_create_params()
|
||||||
|
{
|
||||||
|
var factory = new FakeAbCipTagFactory();
|
||||||
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||||
|
{
|
||||||
|
Devices =
|
||||||
|
[
|
||||||
|
new AbCipDeviceOptions(
|
||||||
|
HostAddress: "ab://10.0.0.6/",
|
||||||
|
PlcFamily: AbCipPlcFamily.Micro800,
|
||||||
|
AddressingMode: AddressingMode.Logical),
|
||||||
|
],
|
||||||
|
Probe = new AbCipProbeOptions { Enabled = false },
|
||||||
|
Tags =
|
||||||
|
[
|
||||||
|
new AbCipTagDefinition("Speed", "ab://10.0.0.6/", "Speed", AbCipDataType.DInt),
|
||||||
|
],
|
||||||
|
OnWarning = _ => { },
|
||||||
|
}, "drv-1", tagFactory: factory);
|
||||||
|
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||||
|
|
||||||
|
factory.Tags["Speed"].CreationParams.AddressingMode.ShouldBe(AddressingMode.Symbolic);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Logical_on_ControlLogix_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.ControlLogix,
|
||||||
|
AddressingMode: AddressingMode.Logical),
|
||||||
|
],
|
||||||
|
Probe = new AbCipProbeOptions { Enabled = false },
|
||||||
|
OnWarning = warnings.Add,
|
||||||
|
}, "drv-1");
|
||||||
|
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
|
||||||
|
warnings.ShouldBeEmpty();
|
||||||
|
drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Logical);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Family-profile compatibility flags ----
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Family_profiles_advertise_logical_support_correctly()
|
||||||
|
{
|
||||||
|
AbCipPlcFamilyProfile.ControlLogix.SupportsLogicalAddressing.ShouldBeTrue();
|
||||||
|
AbCipPlcFamilyProfile.CompactLogix.SupportsLogicalAddressing.ShouldBeTrue();
|
||||||
|
AbCipPlcFamilyProfile.GuardLogix.SupportsLogicalAddressing.ShouldBeTrue();
|
||||||
|
AbCipPlcFamilyProfile.Micro800.SupportsLogicalAddressing.ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- DTO round-trip ----
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DTO_round_trips_AddressingMode_Logical_through_config_json()
|
||||||
|
{
|
||||||
|
var json = """
|
||||||
|
{
|
||||||
|
"Devices": [
|
||||||
|
{
|
||||||
|
"HostAddress": "ab://10.0.0.5/1,0",
|
||||||
|
"PlcFamily": "ControlLogix",
|
||||||
|
"AddressingMode": "Logical"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Probe": { "Enabled": false }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
var drv = AbCipDriverFactoryExtensions.CreateInstance("drv-1", json);
|
||||||
|
await drv.InitializeAsync(json, CancellationToken.None);
|
||||||
|
drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Logical);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DTO_round_trips_AddressingMode_Symbolic_through_config_json()
|
||||||
|
{
|
||||||
|
var json = """
|
||||||
|
{
|
||||||
|
"Devices": [
|
||||||
|
{
|
||||||
|
"HostAddress": "ab://10.0.0.5/1,0",
|
||||||
|
"PlcFamily": "ControlLogix",
|
||||||
|
"AddressingMode": "Symbolic"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Probe": { "Enabled": false }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
var drv = AbCipDriverFactoryExtensions.CreateInstance("drv-1", json);
|
||||||
|
await drv.InitializeAsync(json, CancellationToken.None);
|
||||||
|
drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Symbolic);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DTO_omitted_AddressingMode_falls_back_to_Auto_then_Symbolic()
|
||||||
|
{
|
||||||
|
// No AddressingMode in JSON → DTO field is null → factory parses fallback Auto →
|
||||||
|
// device-level resolution lands on Symbolic.
|
||||||
|
var json = """
|
||||||
|
{
|
||||||
|
"Devices": [
|
||||||
|
{
|
||||||
|
"HostAddress": "ab://10.0.0.5/1,0",
|
||||||
|
"PlcFamily": "ControlLogix"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Probe": { "Enabled": false }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
var drv = AbCipDriverFactoryExtensions.CreateInstance("drv-1", json);
|
||||||
|
await drv.InitializeAsync(json, CancellationToken.None);
|
||||||
|
drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Symbolic);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Logical-mode triggers a one-time symbol walk ----
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Logical_mode_first_read_triggers_symbol_walk_once()
|
||||||
|
{
|
||||||
|
var enumStub = new RecordingEnumeratorFactory(
|
||||||
|
new AbCipDiscoveredTag("Speed", null, AbCipDataType.DInt, false),
|
||||||
|
new AbCipDiscoveredTag("Counter", null, AbCipDataType.DInt, false));
|
||||||
|
var factory = new FakeAbCipTagFactory();
|
||||||
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||||
|
{
|
||||||
|
Devices =
|
||||||
|
[
|
||||||
|
new AbCipDeviceOptions(
|
||||||
|
HostAddress: "ab://10.0.0.5/1,0",
|
||||||
|
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||||
|
AddressingMode: AddressingMode.Logical),
|
||||||
|
],
|
||||||
|
Probe = new AbCipProbeOptions { Enabled = false },
|
||||||
|
Tags =
|
||||||
|
[
|
||||||
|
new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt),
|
||||||
|
new AbCipTagDefinition("Counter", "ab://10.0.0.5/1,0", "Counter", AbCipDataType.DInt),
|
||||||
|
],
|
||||||
|
}, "drv-1", tagFactory: factory, enumeratorFactory: enumStub);
|
||||||
|
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
// First read fires the walk
|
||||||
|
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||||
|
// Second read must NOT walk again
|
||||||
|
await drv.ReadAsync(["Counter"], CancellationToken.None);
|
||||||
|
|
||||||
|
enumStub.CreateCount.ShouldBe(1);
|
||||||
|
var device = drv.GetDeviceState("ab://10.0.0.5/1,0")!;
|
||||||
|
device.LogicalWalkComplete.ShouldBeTrue();
|
||||||
|
device.LogicalInstanceMap.ShouldContainKey("Speed");
|
||||||
|
device.LogicalInstanceMap.ShouldContainKey("Counter");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Symbolic_mode_does_not_trigger_symbol_walk()
|
||||||
|
{
|
||||||
|
var enumStub = new RecordingEnumeratorFactory();
|
||||||
|
var factory = new FakeAbCipTagFactory();
|
||||||
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||||
|
{
|
||||||
|
Devices =
|
||||||
|
[
|
||||||
|
new AbCipDeviceOptions(
|
||||||
|
HostAddress: "ab://10.0.0.5/1,0",
|
||||||
|
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||||
|
AddressingMode: AddressingMode.Symbolic),
|
||||||
|
],
|
||||||
|
Probe = new AbCipProbeOptions { Enabled = false },
|
||||||
|
Tags =
|
||||||
|
[
|
||||||
|
new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt),
|
||||||
|
],
|
||||||
|
}, "drv-1", tagFactory: factory, enumeratorFactory: enumStub);
|
||||||
|
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||||
|
|
||||||
|
enumStub.CreateCount.ShouldBe(0);
|
||||||
|
drv.GetDeviceState("ab://10.0.0.5/1,0")!.LogicalWalkComplete.ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Stubs ----
|
||||||
|
|
||||||
|
private sealed class EmptyEnumeratorFactoryStub : IAbCipTagEnumeratorFactory
|
||||||
|
{
|
||||||
|
public IAbCipTagEnumerator Create() => new EmptyStub();
|
||||||
|
|
||||||
|
private sealed class EmptyStub : IAbCipTagEnumerator
|
||||||
|
{
|
||||||
|
public async IAsyncEnumerable<AbCipDiscoveredTag> EnumerateAsync(
|
||||||
|
AbCipTagCreateParams deviceParams,
|
||||||
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await Task.CompletedTask;
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
public void Dispose() { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingEnumeratorFactory : IAbCipTagEnumeratorFactory
|
||||||
|
{
|
||||||
|
private readonly AbCipDiscoveredTag[] _seed;
|
||||||
|
public int CreateCount;
|
||||||
|
|
||||||
|
public RecordingEnumeratorFactory(params AbCipDiscoveredTag[] seed) => _seed = seed;
|
||||||
|
|
||||||
|
public IAbCipTagEnumerator Create()
|
||||||
|
{
|
||||||
|
CreateCount++;
|
||||||
|
return new SeededStub(_seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SeededStub(AbCipDiscoveredTag[] seed) : IAbCipTagEnumerator
|
||||||
|
{
|
||||||
|
public async IAsyncEnumerable<AbCipDiscoveredTag> EnumerateAsync(
|
||||||
|
AbCipTagCreateParams deviceParams,
|
||||||
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
foreach (var tag in seed)
|
||||||
|
yield return tag;
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
public void Dispose() { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user