fix(abcip): a scalar read whose width disagrees with its authored type is BadTypeMismatch

libplctag decodes whatever bytes the controller returns as the authored type and never
checks the CIP type code. Live read-only check on a production press: WW_ActualVacuum is a
REAL (L5X + the controller's symbol table agree) but the legacy Kepware export in the
ignitionoee repo types it Short; read as INT it returned Good -17346 while the real value
was 5.37. A scalar atomic read (not an array, not a bit-in-DINT) whose buffer size is not
the authored type's width now reads BadTypeMismatch with a warning naming both. Widths
observed live: SINT/BOOL 1, INT 2, DINT/REAL 4, LINT 8. IAbCipTagRuntime.GetSize() is a
default-interface member (-1 = unknown, never checked). Same-width mismatches (DINT vs
REAL) stay undetectable without the type code libplctag strips.
This commit is contained in:
Joseph Doherty
2026-10-07 14:15:19 -04:00
parent 68cf3ae8b7
commit e3d6b76ede
5 changed files with 118 additions and 0 deletions
@@ -799,6 +799,21 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
var tagPath = AbCipTagPath.TryParse(def.TagPath);
var bitIndex = tagPath?.BitIndex;
// libplctag never checks the CIP type the controller returned; it decodes the bytes as
// the authored type. A scalar whose buffer is not that type's width is mis-typed config
// (seen live: a REAL authored as INT read Good -17346), so refuse it rather than publish
// garbage as Good. Same-width mismatches (DINT vs REAL) cannot be seen this way.
if (!IsArrayTag(def) && bitIndex is null
&& ExpectedScalarWidth(def.DataType) is { } width
&& runtime.GetSize() is var size && size >= 0 && size != width)
{
results[fb.OriginalIndex] = new DataValueSnapshot(null, AbCipStatusMapper.BadTypeMismatch, null, now);
_logger.LogWarning(
"AbCip read of {Tag} on device {Device} returned {Size} bytes but its authored type {DataType} is {Width} bytes wide; check the tag's dataType",
reference, def.DeviceHostAddress, size, def.DataType, width);
return ReadOutcome.Degraded($"type mismatch reading {reference}: {size} bytes for {def.DataType}");
}
// Review I-1 — an array tag (the EXPLICIT IsArray flag) decodes the whole buffer into an
// element-typed CLR array (int[]/float[]/bool[]/string[]…), INCLUDING a 1-element array
// (ElementCount 1). Scalar tags keep the single-value path.
@@ -1367,6 +1382,21 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
memberName.StartsWith("ZZZZZZZZZZ", StringComparison.Ordinal)
|| memberName.StartsWith("__", StringComparison.Ordinal);
/// <summary>
/// Wire width of a scalar atomic Logix type, or <c>null</c> where the read buffer's size is not
/// a reliable type check (BOOL, STRING, DT, structures).
/// </summary>
/// <param name="type">The authored type.</param>
/// <returns>The width in bytes, or <c>null</c>.</returns>
internal static int? ExpectedScalarWidth(AbCipDataType type) => type switch
{
AbCipDataType.SInt or AbCipDataType.USInt => 1,
AbCipDataType.Int or AbCipDataType.UInt => 2,
AbCipDataType.DInt or AbCipDataType.UDInt or AbCipDataType.Real => 4,
AbCipDataType.LInt or AbCipDataType.ULInt or AbCipDataType.LReal => 8,
_ => null,
};
/// <summary>
/// Recursively fan a controller-discovered UDT shape out into Variable nodes — one per
/// ATOMIC leaf — under the folder produced by <paramref name="folder"/>. Mirrors the
@@ -31,6 +31,13 @@ public interface IAbCipTagRuntime : IDisposable
/// <returns>The raw libplctag status code (zero on success, negative on error).</returns>
int GetStatus();
/// <summary>
/// Bytes in the last read's buffer, or <c>-1</c> when the runtime cannot say. Used to catch an
/// authored type whose width disagrees with what the controller actually returned.
/// </summary>
/// <returns>The buffer size in bytes, or -1 if unknown.</returns>
int GetSize() => -1;
/// <summary>
/// Decode the local buffer into a boxed .NET value per the tag's configured type.
/// <paramref name="bitIndex"/> is non-null only for BOOL-within-DINT tags captured in
@@ -46,6 +46,9 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
/// <inheritdoc />
public int GetStatus() => (int)_tag.GetStatus();
/// <inheritdoc />
public int GetSize() => _tag.GetSize();
/// <inheritdoc />
public object? DecodeValue(AbCipDataType type, int? bitIndex) => DecodeValueAt(type, 0, bitIndex);