feat(abcip): native tag runtime over the managed CIP transport, with returned-type checking
This commit is contained in:
@@ -1171,7 +1171,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
// path then boxes them into a typed CLR array. Scalar tags pass count 1 + IsArray false.
|
||||
var runtime = _tagFactory.Create(
|
||||
device.BuildCreateParams(
|
||||
parsed.ToLibplctagName(), _options.Timeout, def.ElementCount, IsArrayTag(def)));
|
||||
parsed.ToLibplctagName(), _options.Timeout, def.ElementCount, IsArrayTag(def),
|
||||
authoredType: def.DataType));
|
||||
try
|
||||
{
|
||||
await runtime.InitializeAsync(ct).ConfigureAwait(false);
|
||||
@@ -1686,9 +1687,12 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
/// length for a 1-D array tag (Phase 4c). Coerced to a minimum of 1.</param>
|
||||
/// <param name="isArray">Review I-1 — the EXPLICIT array signal threaded through so a
|
||||
/// 1-element array (<paramref name="elementCount"/> 1) is still read as an array.</param>
|
||||
/// <param name="authoredType">The tag definition's authored type, for the native runtime's
|
||||
/// returned-type check; null skips it.</param>
|
||||
/// <returns>The computed tag creation parameters.</returns>
|
||||
public AbCipTagCreateParams BuildCreateParams(
|
||||
string tagName, TimeSpan timeout, int elementCount = 1, bool isArray = false) => new(
|
||||
string tagName, TimeSpan timeout, int elementCount = 1, bool isArray = false,
|
||||
AbCipDataType? authoredType = null) => new(
|
||||
Gateway: ParsedAddress.Gateway,
|
||||
Port: ParsedAddress.Port,
|
||||
CipPath: ParsedAddress.CipPath,
|
||||
@@ -1698,7 +1702,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
AllowPacking: Options.AllowPacking ?? Profile.SupportsRequestPacking,
|
||||
ConnectionSize: Options.ConnectionSize ?? Profile.DefaultConnectionSize,
|
||||
ElementCount: elementCount < 1 ? 1 : elementCount,
|
||||
IsArray: isArray);
|
||||
IsArray: isArray,
|
||||
AuthoredType: authoredType,
|
||||
UseConnectedMessaging: Profile.SupportsConnectedMessaging);
|
||||
|
||||
/// <summary>Disposes all runtime tag handles and clears the caches.</summary>
|
||||
public void DisposeHandles()
|
||||
|
||||
@@ -119,7 +119,8 @@ public sealed class AbCipDriverProbe : IDriverProbe
|
||||
TagName: tagName,
|
||||
Timeout: timeout,
|
||||
AllowPacking: firstDevice?.AllowPacking ?? profile.SupportsRequestPacking,
|
||||
ConnectionSize: firstDevice?.ConnectionSize ?? profile.DefaultConnectionSize);
|
||||
ConnectionSize: firstDevice?.ConnectionSize ?? profile.DefaultConnectionSize,
|
||||
UseConnectedMessaging: profile.SupportsConnectedMessaging);
|
||||
|
||||
var rt = _tagFactory.Create(p);
|
||||
try
|
||||
|
||||
@@ -119,6 +119,13 @@ public interface IAbCipTagFactory
|
||||
/// element count for an <c>isArray</c> tag — including a 1-element array.</param>
|
||||
/// <param name="IsArray">Review I-1 — the EXPLICIT array signal threaded from the tag definition so
|
||||
/// a 1-element array (<see cref="ElementCount"/> 1) is still read as an array, not a scalar.</param>
|
||||
/// <param name="AuthoredType">The tag definition's authored data type, when known. The native runtime
|
||||
/// checks the CIP type code the controller returns against it (a REAL authored as DINT is
|
||||
/// <c>TypeMismatch</c>); libplctag ignores it. Null (enumeration, probes, BOOL-in-DINT parents)
|
||||
/// skips the check.</param>
|
||||
/// <param name="UseConnectedMessaging">From <c>AbCipPlcFamilyProfile.SupportsConnectedMessaging</c> at
|
||||
/// every construction site, so the family profile stays the single authority for the native
|
||||
/// transport's routing choice. libplctag ignores it.</param>
|
||||
public sealed record AbCipTagCreateParams(
|
||||
string Gateway,
|
||||
int Port,
|
||||
@@ -129,7 +136,9 @@ public sealed record AbCipTagCreateParams(
|
||||
bool AllowPacking = true,
|
||||
int ConnectionSize = 4002,
|
||||
int ElementCount = 1,
|
||||
bool IsArray = false)
|
||||
bool IsArray = false,
|
||||
AbCipDataType? AuthoredType = null,
|
||||
bool UseConnectedMessaging = true)
|
||||
{
|
||||
/// <summary>
|
||||
/// The value for libplctag's <c>gateway</c> attribute: <c>host:port</c>, or bare <c>host</c> when
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
|
||||
/// <summary>
|
||||
/// Maps <see cref="AbCipTagCreateParams"/> onto the native transport's connection identity and
|
||||
/// settings. Shared by the native tag runtime, enumerator and template reader so every handle on
|
||||
/// one device leases the same pooled connection.
|
||||
/// </summary>
|
||||
internal static class NativeAbCipDevice
|
||||
{
|
||||
/// <summary>The EtherNet/IP default TCP port, used when no usable port was configured.</summary>
|
||||
internal const int DefaultPort = 44818;
|
||||
|
||||
/// <summary>The <c>plc=</c> attribute of the Micro800 family profile.</summary>
|
||||
private const string Micro800Attribute = "micro800";
|
||||
|
||||
/// <summary>The pool key: one CIP connection per <c>(host, port, cipPath, plcAttribute)</c>.</summary>
|
||||
/// <param name="p">The tag creation parameters.</param>
|
||||
/// <returns>The device key.</returns>
|
||||
public static DeviceKey Key(AbCipTagCreateParams p) =>
|
||||
new(p.Gateway, p.Port > 0 ? p.Port : DefaultPort, p.CipPath, p.LibplctagPlcAttribute);
|
||||
|
||||
/// <summary>The connection settings for the device <paramref name="p"/> addresses.</summary>
|
||||
/// <param name="p">The tag creation parameters.</param>
|
||||
/// <returns>The connection options.</returns>
|
||||
public static ConnectionOptions Options(AbCipTagCreateParams p) => new()
|
||||
{
|
||||
Routing = RoutingFor(p),
|
||||
Timeout = p.Timeout,
|
||||
ConnectionSize = p.ConnectionSize,
|
||||
AllowPacking = p.AllowPacking,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// How requests reach the controller's message router.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Micro800 is <b>always</b> <see cref="CipRouting.Connected"/> with an empty route, even though its
|
||||
/// family profile says <c>SupportsConnectedMessaging: false</c>: libplctag opens a Large Forward
|
||||
/// Open with connection path <c>20 02 24 01</c> and reads connected (Captures/micro800-read-dint.hex,
|
||||
/// Captures/README.md "Routing observed"; ruling R12-1 — the capture beats the profile flag).
|
||||
/// Every other family follows the profile: connected-capable ⇒ Forward Open; otherwise
|
||||
/// Unconnected Send when a route is configured, bare SendRRData when it is not.
|
||||
/// </remarks>
|
||||
/// <param name="p">The tag creation parameters.</param>
|
||||
/// <returns>The routing mode.</returns>
|
||||
internal static CipRouting RoutingFor(AbCipTagCreateParams p)
|
||||
{
|
||||
if (string.Equals(p.LibplctagPlcAttribute, Micro800Attribute, StringComparison.OrdinalIgnoreCase))
|
||||
return CipRouting.Connected;
|
||||
if (p.UseConnectedMessaging) return CipRouting.Connected;
|
||||
return string.IsNullOrWhiteSpace(p.CipPath) ? CipRouting.Direct : CipRouting.UnconnectedSend;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IAbCipTagFactory"/> for the native transport: every runtime leases its device's
|
||||
/// connection from <see cref="DeviceSessionPool.Shared"/>, so all tags on one device share one CIP
|
||||
/// connection and one request scheduler. Stateless; safe to share across devices.
|
||||
/// </summary>
|
||||
internal sealed class NativeAbCipTagFactory : IAbCipTagFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IAbCipTagRuntime Create(AbCipTagCreateParams createParams) =>
|
||||
new NativeAbCipTagRuntime(createParams, DeviceSessionPool.Shared);
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Logix;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
|
||||
/// <summary>
|
||||
/// Native <see cref="IAbCipTagRuntime"/> over the managed EtherNet/IP + CIP stack. Leases the
|
||||
/// device's shared connection from <see cref="DeviceSessionPool.Shared"/> and reads/writes through a
|
||||
/// per-runtime <see cref="LogixTagClient"/> over the lease's <b>shared</b> scheduler, so concurrent
|
||||
/// tags on one device coalesce into Multiple Service Packets (review B1). Decoding mirrors
|
||||
/// <see cref="LibplctagTagRuntime"/> byte for byte; unlike libplctag it also checks the CIP type
|
||||
/// code the controller returned against the authored type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Not thread-safe: the driver serialises Read → GetStatus → Decode (and Encode → Write → GetStatus)
|
||||
/// per runtime under its per-runtime lock (05/STAB-4), exactly as it does for libplctag.
|
||||
/// </remarks>
|
||||
internal sealed class NativeAbCipTagRuntime : IAbCipTagRuntime
|
||||
{
|
||||
/// <summary>Upper bound on waiting for the lease release (and its Forward Close) in <see cref="Dispose"/>.</summary>
|
||||
private static readonly TimeSpan DisposeWait = TimeSpan.FromSeconds(2);
|
||||
|
||||
/// <summary>Bytes of a Logix STRING's length word.</summary>
|
||||
private const int StringLengthBytes = 4;
|
||||
|
||||
private readonly AbCipTagCreateParams _p;
|
||||
private readonly DeviceSessionPool? _pool;
|
||||
private readonly int? _implicitBit;
|
||||
private readonly byte[] _path;
|
||||
private readonly ushort _elements;
|
||||
|
||||
private ConnectionLease? _lease;
|
||||
private RequestScheduler? _privateScheduler; // ForTest only — production always uses the lease's
|
||||
private LogixTagClient? _client;
|
||||
|
||||
private byte[] _data = [];
|
||||
private ushort? _typeCode;
|
||||
private ushort? _structHandle;
|
||||
private AbStatus _status = AbStatus.Pending;
|
||||
|
||||
private byte[]? _writeBuffer;
|
||||
private string? _pendingString;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>Production constructor; <see cref="InitializeAsync"/> leases the connection from <paramref name="pool"/>.</summary>
|
||||
/// <param name="p">The tag creation parameters.</param>
|
||||
/// <param name="pool">The connection pool (the driver passes <see cref="DeviceSessionPool.Shared"/>).</param>
|
||||
internal NativeAbCipTagRuntime(AbCipTagCreateParams p, DeviceSessionPool pool)
|
||||
: this(p)
|
||||
{
|
||||
_pool = pool ?? throw new ArgumentNullException(nameof(pool));
|
||||
}
|
||||
|
||||
private NativeAbCipTagRuntime(AbCipTagCreateParams p)
|
||||
{
|
||||
_p = p ?? throw new ArgumentNullException(nameof(p));
|
||||
var (name, bit) = CipPath.SplitBitSelector(p.TagName);
|
||||
_implicitBit = bit;
|
||||
_path = CipPath.EncodeSymbolic(name);
|
||||
_elements = (ushort)Math.Clamp(p.ElementCount, 1, ushort.MaxValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test seam: a runtime over <paramref name="channel"/> with a private scheduler and no pool
|
||||
/// lease. Production code always goes through <see cref="NativeAbCipTagFactory"/>.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel to send on.</param>
|
||||
/// <param name="p">The tag creation parameters.</param>
|
||||
/// <returns>The runtime.</returns>
|
||||
internal static NativeAbCipTagRuntime ForTest(ICipChannel channel, AbCipTagCreateParams p)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(channel);
|
||||
var rt = new NativeAbCipTagRuntime(p);
|
||||
rt._privateScheduler = new RequestScheduler(channel, p.AllowPacking);
|
||||
rt._client = new LogixTagClient(rt._privateScheduler, channel);
|
||||
return rt;
|
||||
}
|
||||
|
||||
/// <summary>The CIP type code of the last successful read, if any.</summary>
|
||||
internal ushort? TypeCode => _typeCode;
|
||||
|
||||
/// <summary>The structure handle of the last successful read, if the controller returned one.</summary>
|
||||
internal ushort? StructHandle => _structHandle;
|
||||
|
||||
/// <summary>The trailing bit selector split off the tag name (<c>Tag.5</c>), if any.</summary>
|
||||
internal int? ImplicitBit => _implicitBit;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// Performs one read to prove the tag exists: libplctag's <c>InitializeAsync</c> errors for an
|
||||
/// unknown tag, and the driver's 05/STAB-8 backoff classification depends on create throwing
|
||||
/// <see cref="AbStatus.NotFound"/>.
|
||||
/// </remarks>
|
||||
public async Task InitializeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_client is null)
|
||||
{
|
||||
var lease = _pool!.Acquire(NativeAbCipDevice.Key(_p), NativeAbCipDevice.Options(_p));
|
||||
_lease = lease;
|
||||
// The lease's shared scheduler — never a private one (B1: MSP coalescing across tags).
|
||||
_client = new LogixTagClient(lease.Scheduler, lease.Channel);
|
||||
}
|
||||
await ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ReadAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var client = Client();
|
||||
try
|
||||
{
|
||||
var (type, handle, data) = await client.ReadAsync(_path, _elements, cancellationToken).ConfigureAwait(false);
|
||||
_data = data;
|
||||
_typeCode = type;
|
||||
_structHandle = handle;
|
||||
_status = IsTypeMismatch(type) ? AbStatus.TypeMismatch : AbStatus.Ok;
|
||||
}
|
||||
catch (AbTransportException ex)
|
||||
{
|
||||
// libplctag reports a failed operation by exception AND leaves the status readable.
|
||||
_status = ex.Status;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task WriteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var client = Client();
|
||||
try
|
||||
{
|
||||
if (_pendingString is { } text)
|
||||
{
|
||||
// A STRING writes as the type the controller reported — a Logix STRING is a structure
|
||||
// (0x02A0 + handle); ab_server reports 0x00D0 with no handle. Learn it by reading first
|
||||
// when it is not yet known (we run under the driver's per-runtime lock).
|
||||
if (_typeCode is null || (_typeCode == LogixTypeCode.Struct && _structHandle is null))
|
||||
await ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (_typeCode == LogixTypeCode.Struct && _structHandle is null)
|
||||
throw new AbTransportException(AbStatus.Unsupported,
|
||||
$"STRING write to '{_p.TagName}': the controller returned no structure handle.");
|
||||
var payload = EncodeStringPayload(text);
|
||||
await client.WriteAsync(_path, _typeCode!.Value, _structHandle, 1, payload, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else if (_writeBuffer is { } buffer)
|
||||
{
|
||||
var type = _typeCode ?? AtomicCode(_p.AuthoredType)
|
||||
?? throw new AbTransportException(AbStatus.Unsupported,
|
||||
$"Write to '{_p.TagName}': type unknown — no prior read and no authored atomic type.");
|
||||
var width = LogixTypeCode.ByteWidth(type);
|
||||
if (width != buffer.Length)
|
||||
throw new AbTransportException(AbStatus.TypeMismatch,
|
||||
$"Write to '{_p.TagName}': encoded {buffer.Length} byte(s) but the controller's type 0x{type:X4} is {width} byte(s) wide.");
|
||||
await client.WriteAsync(_path, type, null, 1, buffer, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("WriteAsync called before EncodeValue.");
|
||||
}
|
||||
_status = AbStatus.Ok;
|
||||
}
|
||||
catch (AbTransportException ex)
|
||||
{
|
||||
_status = ex.Status;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int GetStatus() => (int)_status;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int GetSize() => _data.Length;
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? DecodeValue(AbCipDataType type, int? bitIndex) => DecodeValueAt(type, 0, bitIndex);
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? DecodeValueAt(AbCipDataType type, int offset, int? bitIndex) =>
|
||||
_status == AbStatus.TypeMismatch ? null : DecodeAt(type, offset, bitIndex);
|
||||
|
||||
private object? DecodeAt(AbCipDataType type, int offset, int? bitIndex)
|
||||
{
|
||||
// libplctag on a bit tag ("Tag.5"): every integer getter returns that bit, and GetBit ignores
|
||||
// the index it is given in favour of the tag's own bit.
|
||||
if (_implicitBit is int implicitBit && IsIntegerShaped(type))
|
||||
return BoxBit(type, GetBit(implicitBit));
|
||||
|
||||
return type switch
|
||||
{
|
||||
AbCipDataType.Bool => bitIndex is int bit ? GetBit(bit) : Span(offset, 1)[0] != 0,
|
||||
AbCipDataType.SInt => (int)(sbyte)Span(offset, 1)[0],
|
||||
AbCipDataType.USInt => (int)Span(offset, 1)[0],
|
||||
AbCipDataType.Int => (int)BinaryPrimitives.ReadInt16LittleEndian(Span(offset, 2)),
|
||||
AbCipDataType.UInt => (int)BinaryPrimitives.ReadUInt16LittleEndian(Span(offset, 2)),
|
||||
AbCipDataType.DInt or AbCipDataType.Dt => BinaryPrimitives.ReadInt32LittleEndian(Span(offset, 4)),
|
||||
AbCipDataType.UDInt => BinaryPrimitives.ReadUInt32LittleEndian(Span(offset, 4)),
|
||||
AbCipDataType.LInt => BinaryPrimitives.ReadInt64LittleEndian(Span(offset, 8)),
|
||||
AbCipDataType.ULInt => BinaryPrimitives.ReadUInt64LittleEndian(Span(offset, 8)),
|
||||
AbCipDataType.Real => BinaryPrimitives.ReadSingleLittleEndian(Span(offset, 4)),
|
||||
AbCipDataType.LReal => BinaryPrimitives.ReadDoubleLittleEndian(Span(offset, 8)),
|
||||
AbCipDataType.String => DecodeString(offset),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? DecodeArray(AbCipDataType type, int count)
|
||||
{
|
||||
if (_status == AbStatus.TypeMismatch) return null;
|
||||
if (count < 1) count = 1;
|
||||
// Same stride rule as libplctag's runtime (which never has ElementSize after a read).
|
||||
var stride = LibplctagTagRuntime.ResolveArrayStride(null, _data.Length, count, type);
|
||||
return type switch
|
||||
{
|
||||
// A Logix BOOL array is bit-packed: element i is bit i of the buffer.
|
||||
AbCipDataType.Bool => BuildBoolArray(count),
|
||||
AbCipDataType.SInt or AbCipDataType.USInt or AbCipDataType.Int or AbCipDataType.UInt
|
||||
or AbCipDataType.DInt or AbCipDataType.Dt => BuildArray<int>(count, stride, type),
|
||||
AbCipDataType.UDInt => BuildArray<uint>(count, stride, type),
|
||||
AbCipDataType.LInt => BuildArray<long>(count, stride, type),
|
||||
AbCipDataType.ULInt => BuildArray<ulong>(count, stride, type),
|
||||
AbCipDataType.Real => BuildArray<float>(count, stride, type),
|
||||
AbCipDataType.LReal => BuildArray<double>(count, stride, type),
|
||||
AbCipDataType.String => BuildArray<string>(count, stride, type),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EncodeValue(AbCipDataType type, int? bitIndex, object? value)
|
||||
{
|
||||
_writeBuffer = null;
|
||||
_pendingString = null;
|
||||
switch (type)
|
||||
{
|
||||
case AbCipDataType.Bool:
|
||||
if (bitIndex is int)
|
||||
{
|
||||
// Same contract as LibplctagTagRuntime: BOOL-within-DINT writes are an RMW the driver
|
||||
// performs on a parent-DINT runtime (AbCipDriver.WriteBitInDIntAsync).
|
||||
throw new NotSupportedException(
|
||||
"BOOL-with-bitIndex writes must go through AbCipDriver.WriteBitInDIntAsync, not NativeAbCipTagRuntime.");
|
||||
}
|
||||
_writeBuffer = [Convert.ToBoolean(value) ? (byte)1 : (byte)0];
|
||||
break;
|
||||
case AbCipDataType.SInt:
|
||||
_writeBuffer = [(byte)Convert.ToSByte(value)];
|
||||
break;
|
||||
case AbCipDataType.USInt:
|
||||
_writeBuffer = [Convert.ToByte(value)];
|
||||
break;
|
||||
case AbCipDataType.Int:
|
||||
_writeBuffer = new byte[2];
|
||||
BinaryPrimitives.WriteInt16LittleEndian(_writeBuffer, Convert.ToInt16(value));
|
||||
break;
|
||||
case AbCipDataType.UInt:
|
||||
_writeBuffer = new byte[2];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(_writeBuffer, Convert.ToUInt16(value));
|
||||
break;
|
||||
case AbCipDataType.DInt:
|
||||
case AbCipDataType.Dt:
|
||||
_writeBuffer = new byte[4];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(_writeBuffer, Convert.ToInt32(value));
|
||||
break;
|
||||
case AbCipDataType.UDInt:
|
||||
_writeBuffer = new byte[4];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(_writeBuffer, Convert.ToUInt32(value));
|
||||
break;
|
||||
case AbCipDataType.LInt:
|
||||
_writeBuffer = new byte[8];
|
||||
BinaryPrimitives.WriteInt64LittleEndian(_writeBuffer, Convert.ToInt64(value));
|
||||
break;
|
||||
case AbCipDataType.ULInt:
|
||||
_writeBuffer = new byte[8];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(_writeBuffer, Convert.ToUInt64(value));
|
||||
break;
|
||||
case AbCipDataType.Real:
|
||||
_writeBuffer = new byte[4];
|
||||
BinaryPrimitives.WriteSingleLittleEndian(_writeBuffer, Convert.ToSingle(value));
|
||||
break;
|
||||
case AbCipDataType.LReal:
|
||||
_writeBuffer = new byte[8];
|
||||
BinaryPrimitives.WriteDoubleLittleEndian(_writeBuffer, Convert.ToDouble(value));
|
||||
break;
|
||||
case AbCipDataType.String:
|
||||
// The payload needs the capacity learned from a read, which WriteAsync may have to do.
|
||||
_pendingString = Convert.ToString(value) ?? string.Empty;
|
||||
break;
|
||||
case AbCipDataType.Structure:
|
||||
throw new NotSupportedException(
|
||||
"Whole-UDT writes are not supported — address individual member paths instead.");
|
||||
default:
|
||||
throw new NotSupportedException($"AbCipDataType {type} not writable.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the connection lease, waiting at most 2 s so an immediate re-init does not open a
|
||||
/// second session to the device while this one's Forward Close is still in flight (review
|
||||
/// finding 22). Never disposes the shared scheduler; the pool does that on the last release.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
try
|
||||
{
|
||||
if (_lease is { } lease)
|
||||
lease.DisposeAsync().AsTask().Wait(DisposeWait);
|
||||
else if (_privateScheduler is { } scheduler)
|
||||
scheduler.DisposeAsync().AsTask().Wait(DisposeWait);
|
||||
}
|
||||
catch (AggregateException)
|
||||
{
|
||||
// A faulted close is the pool's problem to log; Dispose must not throw.
|
||||
}
|
||||
_lease = null;
|
||||
_privateScheduler = null;
|
||||
}
|
||||
|
||||
private LogixTagClient Client()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
return _client ?? throw new InvalidOperationException("InitializeAsync has not been called.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when the controller returned an atomic type other than the authored one. Exempt:
|
||||
/// no authored type, String/Structure, a non-atomic return, a BOOL bit selector against an integer
|
||||
/// host (<c>Flags.3</c> on a DINT), and an authored BOOL answered as 0xD3 (a packed BOOL-array
|
||||
/// DWORD, which is how Logix returns a BOOL-array element).
|
||||
/// </summary>
|
||||
private bool IsTypeMismatch(ushort returned)
|
||||
{
|
||||
if (AtomicCode(_p.AuthoredType) is not { } expected) return false;
|
||||
if (LogixTypeCode.ByteWidth(returned) < 0 || returned == expected) return false;
|
||||
if (_p.AuthoredType == AbCipDataType.Bool)
|
||||
{
|
||||
if (returned == LogixTypeCode.BoolArray) return false;
|
||||
if (_implicitBit is not null && IsIntegerCode(returned)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>The Logix type code an authored type expects, or null when it is exempt from checking.</summary>
|
||||
private static ushort? AtomicCode(AbCipDataType? type) => type switch
|
||||
{
|
||||
AbCipDataType.Bool => LogixTypeCode.Bool,
|
||||
AbCipDataType.SInt => LogixTypeCode.SInt,
|
||||
AbCipDataType.USInt => LogixTypeCode.USInt,
|
||||
AbCipDataType.Int => LogixTypeCode.Int,
|
||||
AbCipDataType.UInt => LogixTypeCode.UInt,
|
||||
AbCipDataType.DInt or AbCipDataType.Dt => LogixTypeCode.DInt,
|
||||
AbCipDataType.UDInt => LogixTypeCode.UDInt,
|
||||
AbCipDataType.LInt => LogixTypeCode.LInt,
|
||||
AbCipDataType.ULInt => LogixTypeCode.ULInt,
|
||||
AbCipDataType.Real => LogixTypeCode.Real,
|
||||
AbCipDataType.LReal => LogixTypeCode.LReal,
|
||||
_ => null, // String, Structure
|
||||
};
|
||||
|
||||
private static bool IsIntegerCode(ushort code) => code is LogixTypeCode.SInt or LogixTypeCode.Int
|
||||
or LogixTypeCode.DInt or LogixTypeCode.LInt or LogixTypeCode.USInt or LogixTypeCode.UInt
|
||||
or LogixTypeCode.UDInt or LogixTypeCode.ULInt;
|
||||
|
||||
private static bool IsIntegerShaped(AbCipDataType type) => type is AbCipDataType.Bool or AbCipDataType.SInt
|
||||
or AbCipDataType.USInt or AbCipDataType.Int or AbCipDataType.UInt or AbCipDataType.DInt
|
||||
or AbCipDataType.UDInt or AbCipDataType.LInt or AbCipDataType.ULInt or AbCipDataType.Dt;
|
||||
|
||||
private static object BoxBit(AbCipDataType type, bool bit) => type switch
|
||||
{
|
||||
AbCipDataType.Bool => bit,
|
||||
AbCipDataType.UDInt => bit ? 1u : 0u,
|
||||
AbCipDataType.LInt => bit ? 1L : 0L,
|
||||
AbCipDataType.ULInt => bit ? 1UL : 0UL,
|
||||
_ => bit ? 1 : 0,
|
||||
};
|
||||
|
||||
/// <summary>Bit <paramref name="bit"/> of the little-endian buffer.</summary>
|
||||
private bool GetBit(int bit)
|
||||
{
|
||||
if (bit < 0 || bit / 8 >= _data.Length)
|
||||
throw new AbTransportException(AbStatus.OutOfBounds,
|
||||
$"Bit {bit} is outside the {_data.Length}-byte buffer of '{_p.TagName}'.");
|
||||
return (_data[bit / 8] & (1 << (bit % 8))) != 0;
|
||||
}
|
||||
|
||||
/// <summary>The <paramref name="length"/> bytes at <paramref name="offset"/>; OutOfBounds like libplctag.</summary>
|
||||
private ReadOnlySpan<byte> Span(int offset, int length)
|
||||
{
|
||||
if (offset < 0 || length > _data.Length - offset)
|
||||
throw new AbTransportException(AbStatus.OutOfBounds,
|
||||
$"Offset {offset} + {length} byte(s) is outside the {_data.Length}-byte buffer of '{_p.TagName}'.");
|
||||
return _data.AsSpan(offset, length);
|
||||
}
|
||||
|
||||
/// <summary>Logix STRING: <c>u32 LEN</c> then ASCII DATA, LEN clamped to what the buffer holds.</summary>
|
||||
private string DecodeString(int offset)
|
||||
{
|
||||
var len = BinaryPrimitives.ReadUInt32LittleEndian(Span(offset, StringLengthBytes));
|
||||
var available = _data.Length - offset - StringLengthBytes;
|
||||
var count = (int)Math.Min(len, (uint)available);
|
||||
return Encoding.ASCII.GetString(_data, offset + StringLengthBytes, count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>u32 len</c> + DATA padded to the capacity learned from the last read (data length − 4), so the
|
||||
/// write carries the whole structure as the controller sized it.
|
||||
/// </summary>
|
||||
private byte[] EncodeStringPayload(string text)
|
||||
{
|
||||
var capacity = _data.Length - StringLengthBytes;
|
||||
var bytes = Encoding.ASCII.GetBytes(text);
|
||||
if (capacity < 0 || bytes.Length > capacity)
|
||||
throw new AbTransportException(AbStatus.TooLarge,
|
||||
$"STRING write to '{_p.TagName}': {bytes.Length} character(s) exceeds the {Math.Max(capacity, 0)}-byte capacity.");
|
||||
var payload = new byte[StringLengthBytes + capacity];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload, (uint)bytes.Length);
|
||||
bytes.CopyTo(payload, StringLengthBytes);
|
||||
return payload;
|
||||
}
|
||||
|
||||
private bool[] BuildBoolArray(int count)
|
||||
{
|
||||
var arr = new bool[count];
|
||||
for (var i = 0; i < count; i++) arr[i] = GetBit(i);
|
||||
return arr;
|
||||
}
|
||||
|
||||
private T[] BuildArray<T>(int count, int stride, AbCipDataType type)
|
||||
{
|
||||
var arr = new T[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var decoded = DecodeAt(type, i * stride, null);
|
||||
arr[i] = decoded is null ? default! : (T)decoded;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user