Auto: abcip-3.2 — symbolic vs logical addressing toggle

Closes #236
This commit is contained in:
Joseph Doherty
2026-04-25 22:58:33 -04:00
parent 73ff10b595
commit 0c6a0d6e50
13 changed files with 1033 additions and 17 deletions

View File

@@ -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;

View File

@@ -38,7 +38,9 @@ public static class AbCipDriverFactoryExtensions
PlcFamily: ParseEnum<AbCipPlcFamily>(d.PlcFamily, "device", driverInstanceId, "PlcFamily",
fallback: AbCipPlcFamily.ControlLogix),
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 }
? [.. 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"/>.
/// </summary>
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

View File

@@ -114,11 +114,58 @@ public sealed class AbCipDriverOptions
/// supported range [500..4002] at <c>InitializeAsync</c>; out-of-range values fault the
/// driver. <c>null</c> uses the family default — back-compat with deployments that haven't
/// touched the knob.</param>
/// <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(
string HostAddress,
AbCipPlcFamily PlcFamily = AbCipPlcFamily.ControlLogix,
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>
/// One AB-backed OPC UA variable. Mirrors the <c>ModbusTagDefinition</c> shape.

View File

@@ -80,6 +80,19 @@ public interface IAbCipTagFactory
/// Bigger packets fit more tags per RTT (higher throughput); smaller packets stay compatible
/// with legacy firmware (v19-and-earlier ControlLogix caps at 504, Micro800 hard-caps at
/// 488).</param>
/// <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(
string Gateway,
int Port,
@@ -89,4 +102,6 @@ public sealed record AbCipTagCreateParams(
TimeSpan Timeout,
int? StringMaxCapacity = null,
int? ElementCount = null,
int ConnectionSize = 4002);
int ConnectionSize = 4002,
AddressingMode AddressingMode = AddressingMode.Symbolic,
uint? LogicalInstanceId = null);

View File

@@ -14,6 +14,8 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
{
private readonly Tag _tag;
private readonly int _connectionSize;
private readonly AddressingMode _addressingMode;
private readonly uint? _logicalInstanceId;
public LibplctagTagRuntime(AbCipTagCreateParams p)
{
@@ -38,6 +40,8 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
if (p.ElementCount is int n && n > 0)
_tag.ElementCount = n;
_connectionSize = p.ConnectionSize;
_addressingMode = p.AddressingMode;
_logicalInstanceId = p.LogicalInstanceId;
}
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)
// are intentionally swallowed so the driver keeps initialising.
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);
@@ -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 object? DecodeValue(AbCipDataType type, int? bitIndex) => DecodeValueAt(type, 0, bitIndex);

View File

@@ -16,7 +16,8 @@ public sealed record AbCipPlcFamilyProfile(
string DefaultCipPath,
bool SupportsRequestPacking,
bool SupportsConnectedMessaging,
int MaxFragmentBytes)
int MaxFragmentBytes,
bool SupportsLogicalAddressing = true)
{
/// <summary>Look up the profile for a configured family.</summary>
public static AbCipPlcFamilyProfile ForFamily(AbCipPlcFamily family) => family switch
@@ -34,7 +35,8 @@ public sealed record AbCipPlcFamilyProfile(
DefaultCipPath: "1,0",
SupportsRequestPacking: true,
SupportsConnectedMessaging: true,
MaxFragmentBytes: 4000);
MaxFragmentBytes: 4000,
SupportsLogicalAddressing: true);
public static readonly AbCipPlcFamilyProfile CompactLogix = new(
LibplctagPlcAttribute: "compactlogix",
@@ -42,15 +44,21 @@ public sealed record AbCipPlcFamilyProfile(
DefaultCipPath: "1,0",
SupportsRequestPacking: 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(
LibplctagPlcAttribute: "micro800",
DefaultConnectionSize: 488, // Micro800 hard cap
DefaultCipPath: "", // no backplane routing
SupportsRequestPacking: false,
SupportsConnectedMessaging: false, // unconnected-only on most models
MaxFragmentBytes: 484);
MaxFragmentBytes: 484,
SupportsLogicalAddressing: false);
public static readonly AbCipPlcFamilyProfile GuardLogix = new(
LibplctagPlcAttribute: "controllogix", // wire protocol identical; safety partition is tag-level
@@ -58,5 +66,6 @@ public sealed record AbCipPlcFamilyProfile(
DefaultCipPath: "1,0",
SupportsRequestPacking: true,
SupportsConnectedMessaging: true,
MaxFragmentBytes: 4000);
MaxFragmentBytes: 4000,
SupportsLogicalAddressing: true);
}