fix(abcip): stride user-string arrays by the read buffer, not the built-in STRING size

Live read-only check against a production CompactLogix press: Program:UN01_Press._ModeNames
(STR0016[32], 4 elements requested) returned an 80-byte buffer with libplctag.NET 1.5.2's
ElementSize unset. The driver fell back to the static 88-byte STRING stride, read element
1 past the buffer, and the whole read failed (ErrorOutOfBounds -> BadCommunicationError).
When the buffer divides evenly by the element count, that quotient is now the stride;
the read returns [undefined, Production, Maintenance, Manual] as the controller holds.
This commit is contained in:
Joseph Doherty
2026-10-07 14:11:08 -04:00
parent 0ca56cb1f6
commit 68cf3ae8b7
2 changed files with 66 additions and 2 deletions
@@ -79,8 +79,7 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
// accounts for STRING capacity + atomic widths. ElementSize is int? on the wrapper — fall
// back to a static size table when it is null/<=0 (e.g. a zero-length read) so the stride
// is never 0.
var elementSize = _tag.ElementSize ?? 0;
var stride = elementSize > 0 ? elementSize : ElementByteSize(type);
var stride = ResolveArrayStride(_tag.ElementSize, _tag.GetSize(), count, type);
return type switch
{
@@ -99,6 +98,26 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
};
}
/// <summary>
/// Byte stride between array elements. libplctag's <c>ElementSize</c> wins when populated, but
/// after a read it is <c>null</c> (observed live, libplctag.NET 1.5.2) — and the static table
/// assumes the built-in 88-byte STRING, which is wrong for a user string type: a <c>STR0016</c>
/// element is 20 bytes, so element 1 was read at offset 88 of an 80-byte buffer and the whole
/// read failed. When the read buffer divides evenly by the element count, that quotient IS the
/// element size, so it is used before the static table.
/// </summary>
/// <param name="elementSize">libplctag's reported element size, if any.</param>
/// <param name="bufferSize">Bytes in the read buffer.</param>
/// <param name="count">Element count requested.</param>
/// <param name="type">Element type.</param>
/// <returns>The stride in bytes.</returns>
internal static int ResolveArrayStride(int? elementSize, int bufferSize, int count, AbCipDataType type)
{
if (elementSize is > 0) return elementSize.Value;
if (count > 0 && bufferSize > 0 && bufferSize % count == 0) return bufferSize / count;
return ElementByteSize(type);
}
private bool[] BuildBoolArray(int count)
{
var arr = new bool[count];
@@ -0,0 +1,45 @@
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
/// <summary>
/// Array element stride. Seen live on a production CompactLogix: reading
/// <c>Program:UN01_Press._ModeNames</c> (<c>STR0016[32]</c>, 4 elements requested) returned an
/// 80-byte buffer with libplctag's <c>ElementSize</c> unset; the driver strode 88 bytes (built-in
/// STRING) and the read failed <c>ErrorOutOfBounds</c>. The real values were
/// "undefined" / "Production" / "Maintenance" / "Manual" at a 20-byte stride.
/// </summary>
[Trait("Category", "Unit")]
public sealed class AbCipArrayStrideTests
{
/// <summary>A user string type's stride comes from the buffer, not the built-in STRING size.</summary>
[Fact]
public void User_string_array_strides_by_buffer_size_over_count()
{
LibplctagTagRuntime.ResolveArrayStride(null, bufferSize: 80, count: 4, AbCipDataType.String).ShouldBe(20);
}
/// <summary>The built-in STRING still strides 88.</summary>
[Fact]
public void Builtin_STRING_array_strides_88()
{
LibplctagTagRuntime.ResolveArrayStride(null, bufferSize: 3 * 88, count: 3, AbCipDataType.String).ShouldBe(88);
}
/// <summary>libplctag's own element size wins when it is populated.</summary>
[Fact]
public void Reported_element_size_wins()
{
LibplctagTagRuntime.ResolveArrayStride(4, bufferSize: 80, count: 4, AbCipDataType.DInt).ShouldBe(4);
}
/// <summary>An empty or uneven buffer falls back to the static per-type size.</summary>
[Fact]
public void Empty_or_uneven_buffer_falls_back_to_the_type_size()
{
LibplctagTagRuntime.ResolveArrayStride(null, bufferSize: 0, count: 4, AbCipDataType.Real).ShouldBe(4);
LibplctagTagRuntime.ResolveArrayStride(null, bufferSize: 81, count: 4, AbCipDataType.String).ShouldBe(88);
}
}