@@ -77,6 +77,10 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
|
||||
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(
|
||||
Gateway: device.ParsedAddress.Gateway,
|
||||
Port: device.ParsedAddress.Port,
|
||||
@@ -84,7 +88,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
||||
TagName: $"@udt/{templateInstanceId}",
|
||||
Timeout: _options.Timeout,
|
||||
ConnectionSize: device.ConnectionSize);
|
||||
ConnectionSize: device.ConnectionSize,
|
||||
AddressingMode: AddressingMode.Symbolic);
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
_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
|
||||
// (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;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// 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
|
||||
@@ -376,6 +420,11 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
|
||||
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(
|
||||
Gateway: state.ParsedAddress.Gateway,
|
||||
Port: state.ParsedAddress.Port,
|
||||
@@ -383,7 +432,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
LibplctagPlcAttribute: state.Profile.LibplctagPlcAttribute,
|
||||
TagName: _options.Probe.ProbeTagPath!,
|
||||
Timeout: _options.Probe.Timeout,
|
||||
ConnectionSize: state.ConnectionSize);
|
||||
ConnectionSize: state.ConnectionSize,
|
||||
AddressingMode: AddressingMode.Symbolic);
|
||||
|
||||
IAbCipTagRuntime? probeRuntime = null;
|
||||
while (!ct.IsCancellationRequested)
|
||||
@@ -470,6 +520,11 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
var now = DateTime.UtcNow;
|
||||
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
|
||||
// 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
|
||||
@@ -486,6 +541,99 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
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(
|
||||
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,
|
||||
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(
|
||||
Gateway: device.ParsedAddress.Gateway,
|
||||
Port: device.ParsedAddress.Port,
|
||||
@@ -561,7 +718,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
||||
TagName: parsedPath.ToLibplctagName(),
|
||||
Timeout: _options.Timeout,
|
||||
ConnectionSize: device.ConnectionSize);
|
||||
ConnectionSize: device.ConnectionSize,
|
||||
AddressingMode: device.AddressingMode,
|
||||
LogicalInstanceId: sliceLogicalId);
|
||||
|
||||
var plan = AbCipArrayReadPlanner.TryBuild(def, parsedPath, baseParams);
|
||||
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;
|
||||
|
||||
// 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(
|
||||
Gateway: device.ParsedAddress.Gateway,
|
||||
Port: device.ParsedAddress.Port,
|
||||
@@ -921,7 +1090,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
||||
TagName: parentTagName,
|
||||
Timeout: _options.Timeout,
|
||||
ConnectionSize: device.ConnectionSize));
|
||||
ConnectionSize: device.ConnectionSize,
|
||||
AddressingMode: device.AddressingMode,
|
||||
LogicalInstanceId: parentLogicalId));
|
||||
try
|
||||
{
|
||||
await runtime.InitializeAsync(ct).ConfigureAwait(false);
|
||||
@@ -949,6 +1120,16 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
?? throw new InvalidOperationException(
|
||||
$"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(
|
||||
Gateway: device.ParsedAddress.Gateway,
|
||||
Port: device.ParsedAddress.Port,
|
||||
@@ -957,7 +1138,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
TagName: parsed.ToLibplctagName(),
|
||||
Timeout: _options.Timeout,
|
||||
StringMaxCapacity: def.DataType == AbCipDataType.String ? def.StringLength : null,
|
||||
ConnectionSize: device.ConnectionSize));
|
||||
ConnectionSize: device.ConnectionSize,
|
||||
AddressingMode: device.AddressingMode,
|
||||
LogicalInstanceId: logicalId));
|
||||
try
|
||||
{
|
||||
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))
|
||||
{
|
||||
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(
|
||||
Gateway: state.ParsedAddress.Gateway,
|
||||
Port: state.ParsedAddress.Port,
|
||||
@@ -1112,7 +1299,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
LibplctagPlcAttribute: state.Profile.LibplctagPlcAttribute,
|
||||
TagName: "@tags",
|
||||
Timeout: _options.Timeout,
|
||||
ConnectionSize: state.ConnectionSize);
|
||||
ConnectionSize: state.ConnectionSize,
|
||||
AddressingMode: AddressingMode.Symbolic);
|
||||
|
||||
IAddressSpaceBuilder? discoveredFolder = null;
|
||||
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(
|
||||
AbCipHostAddress parsedAddress,
|
||||
AbCipDeviceOptions options,
|
||||
AbCipPlcFamilyProfile profile)
|
||||
AbCipPlcFamilyProfile profile,
|
||||
AddressingMode resolvedAddressingMode)
|
||||
{
|
||||
public AbCipHostAddress ParsedAddress { get; } = parsedAddress;
|
||||
public AbCipDeviceOptions Options { get; } = options;
|
||||
@@ -1193,6 +1382,39 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
/// </summary>
|
||||
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 HostState HostState { get; set; } = HostState.Unknown;
|
||||
public DateTime HostStateChangedUtc { get; set; } = DateTime.UtcNow;
|
||||
|
||||
Reference in New Issue
Block a user