From 68cf3ae8b7ee29c7bc9da5fbca4cf711d7927aa8 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 7 Oct 2026 14:11:08 -0400 Subject: [PATCH] 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. --- .../LibplctagTagRuntime.cs | 23 +++++++++- .../AbCipArrayStrideTests.cs | 45 +++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipArrayStrideTests.cs diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs index 11ef9b26..b11cbd49 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs @@ -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 }; } + /// + /// Byte stride between array elements. libplctag's ElementSize wins when populated, but + /// after a read it is null (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 STR0016 + /// 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. + /// + /// libplctag's reported element size, if any. + /// Bytes in the read buffer. + /// Element count requested. + /// Element type. + /// The stride in bytes. + 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]; diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipArrayStrideTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipArrayStrideTests.cs new file mode 100644 index 00000000..86111824 --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipArrayStrideTests.cs @@ -0,0 +1,45 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Driver.AbCip; + +namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; + +/// +/// Array element stride. Seen live on a production CompactLogix: reading +/// Program:UN01_Press._ModeNames (STR0016[32], 4 elements requested) returned an +/// 80-byte buffer with libplctag's ElementSize unset; the driver strode 88 bytes (built-in +/// STRING) and the read failed ErrorOutOfBounds. The real values were +/// "undefined" / "Production" / "Maintenance" / "Manual" at a 20-byte stride. +/// +[Trait("Category", "Unit")] +public sealed class AbCipArrayStrideTests +{ + /// A user string type's stride comes from the buffer, not the built-in STRING size. + [Fact] + public void User_string_array_strides_by_buffer_size_over_count() + { + LibplctagTagRuntime.ResolveArrayStride(null, bufferSize: 80, count: 4, AbCipDataType.String).ShouldBe(20); + } + + /// The built-in STRING still strides 88. + [Fact] + public void Builtin_STRING_array_strides_88() + { + LibplctagTagRuntime.ResolveArrayStride(null, bufferSize: 3 * 88, count: 3, AbCipDataType.String).ShouldBe(88); + } + + /// libplctag's own element size wins when it is populated. + [Fact] + public void Reported_element_size_wins() + { + LibplctagTagRuntime.ResolveArrayStride(4, bufferSize: 80, count: 4, AbCipDataType.DInt).ShouldBe(4); + } + + /// An empty or uneven buffer falls back to the static per-type size. + [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); + } +}