fix(abcip): native runtime write guards, same-width integer exemption, deferred path encoding (Task 12 review r1)
This commit is contained in:
@@ -28,10 +28,13 @@ internal sealed class NativeAbCipTagRuntime : IAbCipTagRuntime
|
||||
|
||||
private readonly AbCipTagCreateParams _p;
|
||||
private readonly DeviceSessionPool? _pool;
|
||||
private readonly int? _implicitBit;
|
||||
private readonly byte[] _path;
|
||||
private readonly ushort _elements;
|
||||
|
||||
// Encoded in InitializeAsync, never in the constructor: a malformed name must surface from
|
||||
// Initialize (inside the driver's / probe's try), not from IAbCipTagFactory.Create — as libplctag.
|
||||
private int? _implicitBit;
|
||||
private byte[]? _path;
|
||||
|
||||
private ConnectionLease? _lease;
|
||||
private RequestScheduler? _privateScheduler; // ForTest only — production always uses the lease's
|
||||
private LogixTagClient? _client;
|
||||
@@ -57,9 +60,6 @@ internal sealed class NativeAbCipTagRuntime : IAbCipTagRuntime
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -97,6 +97,14 @@ internal sealed class NativeAbCipTagRuntime : IAbCipTagRuntime
|
||||
public async Task InitializeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_path is null)
|
||||
{
|
||||
// Before any lease: a malformed name costs no connection.
|
||||
var (name, bit) = CipPath.SplitBitSelector(_p.TagName);
|
||||
var path = CipPath.EncodeSymbolic(name);
|
||||
_implicitBit = bit;
|
||||
_path = path;
|
||||
}
|
||||
if (_client is null)
|
||||
{
|
||||
var lease = _pool!.Acquire(NativeAbCipDevice.Key(_p), NativeAbCipDevice.Options(_p));
|
||||
@@ -111,9 +119,10 @@ internal sealed class NativeAbCipTagRuntime : IAbCipTagRuntime
|
||||
public async Task ReadAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var client = Client();
|
||||
var path = Path();
|
||||
try
|
||||
{
|
||||
var (type, handle, data) = await client.ReadAsync(_path, _elements, cancellationToken).ConfigureAwait(false);
|
||||
var (type, handle, data) = await client.ReadAsync(path,_elements, cancellationToken).ConfigureAwait(false);
|
||||
_data = data;
|
||||
_typeCode = type;
|
||||
_structHandle = handle;
|
||||
@@ -131,6 +140,15 @@ internal sealed class NativeAbCipTagRuntime : IAbCipTagRuntime
|
||||
public async Task WriteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var client = Client();
|
||||
var path = Path();
|
||||
|
||||
// A bit tag ("Flags.3") names one bit of its host word. Writing any encoded value to it would
|
||||
// write the WHOLE host and wipe its other bits on the controller; BOOL bit writes are an RMW the
|
||||
// driver performs on a parent-DINT runtime (WriteBitInDIntAsync), so anything reaching here is refused.
|
||||
if (_implicitBit is int implicitBit)
|
||||
throw new NotSupportedException(
|
||||
$"Write to bit tag '{_p.TagName}' (bit {implicitBit}) is not supported here — bit writes go through AbCipDriver.WriteBitInDIntAsync.");
|
||||
|
||||
try
|
||||
{
|
||||
if (_pendingString is { } text)
|
||||
@@ -144,7 +162,7 @@ internal sealed class NativeAbCipTagRuntime : IAbCipTagRuntime
|
||||
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)
|
||||
await client.WriteAsync(path, _typeCode!.Value, _structHandle, 1, payload, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else if (_writeBuffer is { } buffer)
|
||||
@@ -152,11 +170,25 @@ internal sealed class NativeAbCipTagRuntime : IAbCipTagRuntime
|
||||
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.");
|
||||
// The read rule decides first: a same-width mismatch (REAL under a DINT-authored tag)
|
||||
// would otherwise send the int bit pattern as a REAL and report Good.
|
||||
if (_typeCode is { } returned && IsTypeMismatch(returned))
|
||||
throw new AbTransportException(AbStatus.TypeMismatch,
|
||||
$"Write to '{_p.TagName}': the controller's type 0x{returned:X4} does not match the authored {_p.AuthoredType}.");
|
||||
// Width is checked only for codes with a known width: DATE / DATE_AND_TIME (0xCD/0xCF) and
|
||||
// Micro800 BYTE/WORD/LWORD (0xD1/0xD2/0xD4) are outside LogixTypeCode's table and write as today.
|
||||
var width = LogixTypeCode.ByteWidth(type);
|
||||
if (width != buffer.Length)
|
||||
if (width > 0 && width != buffer.Length)
|
||||
{
|
||||
// An authored BOOL over a packed BOOL-array DWORD: writing the DWORD would clobber the
|
||||
// 31 sibling bools (libplctag does exactly that), so refuse it as a config error.
|
||||
if (type == LogixTypeCode.BoolArray)
|
||||
throw new NotSupportedException(
|
||||
$"Write to '{_p.TagName}': a BOOL-array element (type 0xD3 DWORD) cannot be written as a single BOOL.");
|
||||
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);
|
||||
}
|
||||
await client.WriteAsync(path, type, null, 1, buffer, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -324,6 +356,9 @@ internal sealed class NativeAbCipTagRuntime : IAbCipTagRuntime
|
||||
_privateScheduler = null;
|
||||
}
|
||||
|
||||
private byte[] Path() =>
|
||||
_path ?? throw new InvalidOperationException("InitializeAsync has not been called.");
|
||||
|
||||
private LogixTagClient Client()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
@@ -334,7 +369,10 @@ internal sealed class NativeAbCipTagRuntime : IAbCipTagRuntime
|
||||
/// 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).
|
||||
/// DWORD, which is how Logix returns a BOOL-array element), and same-width integers that differ only
|
||||
/// in signedness or are the Micro800 bit-string type of that width (SINT/USINT/BYTE, INT/UINT/WORD,
|
||||
/// DINT/UDINT/DWORD, LINT/ULINT/LWORD): libplctag reads those Good today and a transport cutover
|
||||
/// must not turn them Bad. Different widths and integer↔real still mismatch.
|
||||
/// </summary>
|
||||
private bool IsTypeMismatch(ushort returned)
|
||||
{
|
||||
@@ -345,9 +383,23 @@ internal sealed class NativeAbCipTagRuntime : IAbCipTagRuntime
|
||||
if (returned == LogixTypeCode.BoolArray) return false;
|
||||
if (_implicitBit is not null && IsIntegerCode(returned)) return false;
|
||||
}
|
||||
if (IntegerWidth(expected) is int width && IntegerWidth(returned) == width) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Micro800 bit-string type codes (DWORD is <see cref="LogixTypeCode.BoolArray"/>, 0xD3).</summary>
|
||||
private const ushort ByteCode = 0xD1, WordCode = 0xD2, LWordCode = 0xD4;
|
||||
|
||||
/// <summary>Width of an integer or bit-string code; null for anything else (BOOL, REAL, ...).</summary>
|
||||
private static int? IntegerWidth(ushort code) => code switch
|
||||
{
|
||||
LogixTypeCode.SInt or LogixTypeCode.USInt or ByteCode => 1,
|
||||
LogixTypeCode.Int or LogixTypeCode.UInt or WordCode => 2,
|
||||
LogixTypeCode.DInt or LogixTypeCode.UDInt or LogixTypeCode.BoolArray => 4,
|
||||
LogixTypeCode.LInt or LogixTypeCode.ULInt or LWordCode => 8,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// <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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user