diff --git a/src/S7CommPlusDriver.Tests/PlcTagMultiDimensionalArrayTests.cs b/src/S7CommPlusDriver.Tests/PlcTagMultiDimensionalArrayTests.cs new file mode 100644 index 0000000..16896a8 --- /dev/null +++ b/src/S7CommPlusDriver.Tests/PlcTagMultiDimensionalArrayTests.cs @@ -0,0 +1,268 @@ +using System; +using System.Linq; +using S7CommPlusDriver.ClientApi; +using Xunit; + +namespace S7CommPlusDriver.Tests +{ + /// + /// Verifies that a multidimensional primitive PLC array (e.g. Array[1..2, 1..3] of Int) is reshaped into a genuine + /// multidimensional CLR array on read, and correctly distributed back into its individual element tags on write, instead of + /// collapsing into (or requiring) a flat one-dimensional array. + /// + /// + /// Regression test for a design gap ported from the upstream fork's e91d8f2 ("Extended DataTypes") commit, which + /// introduced dedicated PlcTagXxxArrayMDim types there. This driver instead extends the existing + /// aggregate-element mechanism (, , + /// ) to reshape/distribute values without introducing new tag types. + /// + public sealed class PlcTagMultiDimensionalArrayTests + { + private static VarInfo CreateTwoByThreeIntArrayVarInfo() + { + return new VarInfo + { + Name = "DB.Matrix", + AccessSequence = "8A0E0001.F", + SymbolCrc = 0x11223344, + Softdatatype = Softdatatype.S7COMMP_SOFTDATATYPE_INT, + ArrayElementCount = 6, + ArrayDimensions = new[] + { + new S7CommPlusArrayDimension(1, 2), + new S7CommPlusArrayDimension(1, 3), + }, + }; + } + + [Fact] + public void CreateResolvedPlcTagReshapesMultiDimensionalReadIntoGenuineMultiDimensionalArray() + { + var tag = S7CommPlusProtocolSession.CreateResolvedPlcTag(CreateTwoByThreeIntArrayVarInfo()); + + Assert.Equal(6, tag.AggregateElements.Count); + for (var index = 0; index < tag.AggregateElements.Count; index++) + { + tag.AggregateElements[index].ProcessReadResult(new ValueInt((short)(index + 1)), 0); + } + tag.CompleteAggregateRead(0); + + var value = Assert.IsType(tag.GetValue()); + Assert.Equal(2, value.GetLength(0)); + Assert.Equal(3, value.GetLength(1)); + // Row-major: outermost (first declared) dimension varies slowest. + Assert.Equal(1, value[0, 0]); + Assert.Equal(2, value[0, 1]); + Assert.Equal(3, value[0, 2]); + Assert.Equal(4, value[1, 0]); + Assert.Equal(5, value[1, 1]); + Assert.Equal(6, value[1, 2]); + } + + [Fact] + public void CreateResolvedPlcTagDistributesMultiDimensionalWriteIntoElementTags() + { + var tag = S7CommPlusProtocolSession.CreateResolvedPlcTag(CreateTwoByThreeIntArrayVarInfo()); + + var writeValue = new short[2, 3] { { 10, 20, 30 }, { 40, 50, 60 } }; + tag.SetValue(writeValue); + + var elementValues = tag.AggregateElements + .Select(element => ((PlcTagInt)element).Value) + .ToArray(); + Assert.Equal(new short[] { 10, 20, 30, 40, 50, 60 }, elementValues); + } + + [Fact] + public void CreateResolvedPlcTagKeepsFlatArrayForOneDimensionalAggregate() + { + var varInfo = new VarInfo + { + Name = "DB.Values", + AccessSequence = "8A0E0001.F", + SymbolCrc = 0x55667788, + Softdatatype = Softdatatype.S7COMMP_SOFTDATATYPE_INT, + ArrayElementCount = 3, + ArrayDimensions = new[] { new S7CommPlusArrayDimension(0, 3) }, + }; + + var tag = S7CommPlusProtocolSession.CreateResolvedPlcTag(varInfo); + for (var index = 0; index < tag.AggregateElements.Count; index++) + { + tag.AggregateElements[index].ProcessReadResult(new ValueInt((short)(index + 1)), 0); + } + tag.CompleteAggregateRead(0); + + var value = Assert.IsType(tag.GetValue()); + Assert.Equal(new short[] { 1, 2, 3 }, value); + } + + [Fact] + public void CreateResolvedPlcTagReshapesMultiDimensionalBoolReadIntoGenuineMultiDimensionalArray() + { + var tag = S7CommPlusProtocolSession.CreateResolvedPlcTag(CreateTwoByTwoVarInfo(Softdatatype.S7COMMP_SOFTDATATYPE_BOOL, "DB.Flags", 0x22334455)); + var expected = new[] { true, false, false, true }; + for (var index = 0; index < tag.AggregateElements.Count; index++) + { + tag.AggregateElements[index].ProcessReadResult(new ValueBool(expected[index]), 0); + } + tag.CompleteAggregateRead(0); + + var value = Assert.IsType(tag.GetValue()); + Assert.Equal(2, value.GetLength(0)); + Assert.Equal(2, value.GetLength(1)); + Assert.True(value[0, 0]); + Assert.False(value[0, 1]); + Assert.False(value[1, 0]); + Assert.True(value[1, 1]); + } + + [Fact] + public void CreateResolvedPlcTagDistributesMultiDimensionalBoolWriteIntoElementTags() + { + var tag = S7CommPlusProtocolSession.CreateResolvedPlcTag(CreateTwoByTwoVarInfo(Softdatatype.S7COMMP_SOFTDATATYPE_BOOL, "DB.Flags", 0x22334455)); + tag.SetValue(new[,] { { true, false }, { false, true } }); + + var elementValues = tag.AggregateElements + .Select(element => ((PlcTagBool)element).Value) + .ToArray(); + Assert.Equal(new[] { true, false, false, true }, elementValues); + } + + [Fact] + public void CreateResolvedPlcTagReshapesMultiDimensionalRealReadIntoGenuineMultiDimensionalArray() + { + var tag = S7CommPlusProtocolSession.CreateResolvedPlcTag(CreateTwoByTwoVarInfo(Softdatatype.S7COMMP_SOFTDATATYPE_REAL, "DB.Measurements", 0x33445566)); + var expected = new[] { 1.5f, 2.5f, 3.5f, 4.5f }; + for (var index = 0; index < tag.AggregateElements.Count; index++) + { + tag.AggregateElements[index].ProcessReadResult(new ValueReal(expected[index]), 0); + } + tag.CompleteAggregateRead(0); + + var value = Assert.IsType(tag.GetValue()); + Assert.Equal(2, value.GetLength(0)); + Assert.Equal(2, value.GetLength(1)); + Assert.Equal(1.5f, value[0, 0]); + Assert.Equal(2.5f, value[0, 1]); + Assert.Equal(3.5f, value[1, 0]); + Assert.Equal(4.5f, value[1, 1]); + } + + [Fact] + public void CreateResolvedPlcTagDistributesMultiDimensionalRealWriteIntoElementTags() + { + var tag = S7CommPlusProtocolSession.CreateResolvedPlcTag(CreateTwoByTwoVarInfo(Softdatatype.S7COMMP_SOFTDATATYPE_REAL, "DB.Measurements", 0x33445566)); + tag.SetValue(new[,] { { 1.5f, 2.5f }, { 3.5f, 4.5f } }); + + var elementValues = tag.AggregateElements + .Select(element => ((PlcTagReal)element).Value) + .ToArray(); + Assert.Equal(new[] { 1.5f, 2.5f, 3.5f, 4.5f }, elementValues); + } + + [Fact] + public void CreateResolvedPlcTagReshapesMultiDimensionalStringReadIntoGenuineMultiDimensionalArray() + { + var varInfo = CreateTwoByTwoVarInfo(Softdatatype.S7COMMP_SOFTDATATYPE_STRING, "DB.Texts", 0x44556677); + varInfo.MaxStringLength = 10; + var tag = S7CommPlusProtocolSession.CreateResolvedPlcTag(varInfo); + var expected = new[] { "aa", "bb", "cc", "dd" }; + for (var index = 0; index < tag.AggregateElements.Count; index++) + { + tag.AggregateElements[index].ProcessReadResult(CreateStringReadValue(expected[index], 10), 0); + } + tag.CompleteAggregateRead(0); + + var value = Assert.IsType(tag.GetValue()); + Assert.Equal(2, value.GetLength(0)); + Assert.Equal(2, value.GetLength(1)); + Assert.Equal("aa", value[0, 0]); + Assert.Equal("bb", value[0, 1]); + Assert.Equal("cc", value[1, 0]); + Assert.Equal("dd", value[1, 1]); + } + + [Fact] + public void CreateResolvedPlcTagDistributesMultiDimensionalStringWriteIntoElementTags() + { + var varInfo = CreateTwoByTwoVarInfo(Softdatatype.S7COMMP_SOFTDATATYPE_STRING, "DB.Texts", 0x44556677); + varInfo.MaxStringLength = 10; + var tag = S7CommPlusProtocolSession.CreateResolvedPlcTag(varInfo); + tag.SetValue(new[,] { { "aa", "bb" }, { "cc", "dd" } }); + + var elementValues = tag.AggregateElements + .Select(element => ((PlcTagString)element).Value) + .ToArray(); + Assert.Equal(new[] { "aa", "bb", "cc", "dd" }, elementValues); + } + + [Fact] + public void CreateResolvedPlcTagReshapesMultiDimensionalDtlStructReadIntoGenuineMultiDimensionalArray() + { + var tag = S7CommPlusProtocolSession.CreateResolvedPlcTag(CreateTwoByTwoVarInfo(Softdatatype.S7COMMP_SOFTDATATYPE_DTL, "DB.Timestamps", 0x66778899)); + var expected = new[] + { + new DateTime(2020, 1, 1), + new DateTime(2020, 1, 2), + new DateTime(2020, 1, 3), + new DateTime(2020, 1, 4), + }; + for (var index = 0; index < tag.AggregateElements.Count; index++) + { + tag.AggregateElements[index].ProcessReadResult(CreateDtlReadValue(expected[index]), 0); + } + tag.CompleteAggregateRead(0); + + var value = Assert.IsType(tag.GetValue()); + Assert.Equal(2, value.GetLength(0)); + Assert.Equal(2, value.GetLength(1)); + Assert.Equal(expected[0], value[0, 0]); + Assert.Equal(expected[1], value[0, 1]); + Assert.Equal(expected[2], value[1, 0]); + Assert.Equal(expected[3], value[1, 1]); + } + + private static VarInfo CreateTwoByTwoVarInfo(uint softdatatype, string name, uint symbolCrc) + { + return new VarInfo + { + Name = name, + AccessSequence = "8A0E0001.F", + SymbolCrc = symbolCrc, + Softdatatype = softdatatype, + ArrayElementCount = 4, + ArrayDimensions = new[] + { + new S7CommPlusArrayDimension(0, 2), + new S7CommPlusArrayDimension(0, 2), + }, + }; + } + + private static ValueUSIntArray CreateStringReadValue(string text, byte maxLength) + { + var bytes = System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(text); + var b = new byte[maxLength + 2]; + b[0] = maxLength; + b[1] = (byte)bytes.Length; + Array.Copy(bytes, 0, b, 2, bytes.Length); + return new ValueUSIntArray(b); + } + + private static ValueStruct CreateDtlReadValue(DateTime value) + { + var structValue = new ValueStruct(0x02000043); + var barr = new byte[12]; + barr[0] = (byte)(value.Year >> 8); + barr[1] = (byte)value.Year; + barr[2] = (byte)value.Month; + barr[3] = (byte)value.Day; + barr[5] = (byte)value.Hour; + barr[6] = (byte)value.Minute; + barr[7] = (byte)value.Second; + structValue.AddStructElement(0x02000043, new ValueByteArray(barr)); + return structValue; + } + } +} diff --git a/src/S7CommPlusDriver.Tests/PlcTagStringMaxLengthTests.cs b/src/S7CommPlusDriver.Tests/PlcTagStringMaxLengthTests.cs new file mode 100644 index 0000000..42921b2 --- /dev/null +++ b/src/S7CommPlusDriver.Tests/PlcTagStringMaxLengthTests.cs @@ -0,0 +1,91 @@ +using S7CommPlusDriver.ClientApi; +using Xunit; + +namespace S7CommPlusDriver.Tests +{ + /// + /// Verifies that forwards the PLC-declared maximum string length to + /// , , and , and that the resulting + /// write telegram matches the declared length instead of always falling back to the built-in default of 254. + /// + /// + /// Regression test for a bug where TagFactory never received the declared maximum string length, so every + /// String/WString tag was created with the built-in default of 254, regardless of its actual PLC + /// declaration (e.g. String[10]). Writing such a tag then sent a telegram of the wrong size and with the wrong + /// declared-length header byte, which the PLC rejected. + /// + public sealed class PlcTagStringMaxLengthTests + { + [Fact] + public void TagFactoryAppliesDeclaredMaxStringLengthToScalarStringTag() + { + var tag = PlcTags.TagFactory( + "DB.Text", + new ItemAddress("8A0E0001.F"), + Softdatatype.S7COMMP_SOFTDATATYPE_STRING, + Is1Dim: false, + maxStringLength: 10); + + var stringTag = Assert.IsType(tag); + stringTag.Value = "0123456789"; // exactly 10 characters, must not throw + + var writeValue = Assert.IsType(GetWriteValue(stringTag)); + var bytes = writeValue.GetValue(); + + Assert.Equal(12, bytes.Length); // declared max length (10) + 2 header bytes + Assert.Equal(10, bytes[0]); // declared max length, not the built-in default of 254 + Assert.Equal(10, bytes[1]); // actual length + } + + [Fact] + public void TagFactoryFallsBackToDefaultMaxStringLengthWhenNotSupplied() + { + var tag = PlcTags.TagFactory( + "DB.Text", + new ItemAddress("8A0E0001.F"), + Softdatatype.S7COMMP_SOFTDATATYPE_STRING); + + var stringTag = Assert.IsType(tag); + stringTag.Value = "Hello"; + + var writeValue = Assert.IsType(GetWriteValue(stringTag)); + var bytes = writeValue.GetValue(); + + Assert.Equal(256, bytes.Length); // built-in default of 254 + 2 header bytes + Assert.Equal(254, bytes[0]); + } + + [Fact] + public void TagFactoryAppliesDeclaredMaxStringLengthToStringArrayElementTag() + { + var tag = PlcTags.TagFactory( + "DB.Texts", + new ItemAddress("8A0E0001.F"), + Softdatatype.S7COMMP_SOFTDATATYPE_STRING, + Is1Dim: true, + maxStringLength: 5); + + Assert.IsType(tag); + Assert.Equal(5, tag.GetMaxStringLength()); + } + + [Fact] + public void TagFactoryAppliesDeclaredMaxStringLengthToWStringTag() + { + var tag = PlcTags.TagFactory( + "DB.UnicodeText", + new ItemAddress("8A0E0001.F"), + Softdatatype.S7COMMP_SOFTDATATYPE_WSTRING, + Is1Dim: false, + maxStringLength: 20); + + var wstringTag = Assert.IsType(tag); + Assert.Equal(20, wstringTag.GetMaxStringLength()); + } + + private static PValue GetWriteValue(PlcTag tag) + { + return tag.GetWriteValue(); + } + } +} diff --git a/src/S7CommPlusDriver/ClientApi/Browser.cs b/src/S7CommPlusDriver/ClientApi/Browser.cs index 8fc194e..d111bbd 100644 --- a/src/S7CommPlusDriver/ClientApi/Browser.cs +++ b/src/S7CommPlusDriver/ClientApi/Browser.cs @@ -174,6 +174,7 @@ private void AddFlatSubnodes( ArrayElementCount = GetArrayElementCount(node), ArrayDimensions = GetArrayDimensions(node), ContainsIndexedArray = containsIndexedArray, + MaxStringLength = node.Vte?.OffsetInfoType?.MaxStringLength() ?? 0, }; // If an Array element of basic datatype, the Vte is here from the parent array base element and offsets not valid here. if (node.NodeType == eNodeType.Array) @@ -940,6 +941,12 @@ public class VarInfo /// public IReadOnlyList ArrayDimensions = Array.Empty(); + /// + /// Gets the maximum string length declared in the PLC for String/WString values, or zero for every + /// other datatype. Required to build a correctly sized write telegram; see . + /// + public int MaxStringLength; + /// Retains the parsed symbol path so tag creation can reuse the exact CRC inputs. internal List SymbolCrcPath; diff --git a/src/S7CommPlusDriver/ClientApi/PlcTag.cs b/src/S7CommPlusDriver/ClientApi/PlcTag.cs index 7cdef54..986e7ec 100644 --- a/src/S7CommPlusDriver/ClientApi/PlcTag.cs +++ b/src/S7CommPlusDriver/ClientApi/PlcTag.cs @@ -18,6 +18,7 @@ namespace S7CommPlusDriver.ClientApi public abstract class PlcTag { private IReadOnlyList m_AggregateElements = Array.Empty(); + private IReadOnlyList m_AggregateDimensions = Array.Empty(); public string Name; public ItemAddress Address; @@ -64,6 +65,48 @@ public virtual void ProcessWriteResult(UInt64 error) internal abstract PValue GetWriteValue(); + /// + /// Gets the PLC-declared maximum string length for , , and + /// tags, or zero for every other tag type. Used to persist and recreate the exact + /// wire length required by (see ). + /// + internal virtual int GetMaxStringLength() => 0; + + /// + /// Gets the tag's current typed value. For a multidimensional aggregate array (see and + /// ), the reshaped multidimensional array is returned because the concrete tag's Value + /// property can only expose a flat one-dimensional array. Every other tag returns its boxed public Value property. + /// + /// The boxed value, or when the concrete tag type has no public Value property. + public virtual object GetValue() + { + if (m_AggregateDimensions.Count > 1) + { + return AggregateValue; + } + var valueProperty = GetType().GetProperty("Value", BindingFlags.Public | BindingFlags.Instance); + return valueProperty?.GetValue(this); + } + + /// + /// Sets the tag's current typed value. For a multidimensional aggregate array, is expected to be + /// a multidimensional matching the PLC-declared dimensions; it is reshaped and distributed directly + /// into the individual because the concrete tag's Value property can only hold a + /// flat one-dimensional array. Every other tag assigns to its public Value property. + /// + /// The value to assign, which must be assignable to the concrete tag's Value property type. + public virtual void SetValue(object value) + { + if (m_AggregateDimensions.Count > 1 && value is Array multiDimValue) + { + DistributeMultiDimensionalValue(multiDimValue); + AggregateValue = multiDimValue; + return; + } + var valueProperty = GetType().GetProperty("Value", BindingFlags.Public | BindingFlags.Instance); + valueProperty?.SetValue(this, value); + } + internal void SetTraceAddressMetadata(VarInfo variable) { if (variable == null) @@ -98,14 +141,21 @@ protected static int CheckErrorAndType(ulong error, object valueObj, Type checkT /// /// Configures the scalar element tags that collectively represent this aggregate PLC array. /// - /// Element tags in PLC declaration order. - internal void SetAggregateElements(IReadOnlyList elements) + /// Element tags in PLC declaration order (row-major, outermost dimension first). + /// + /// The PLC-declared dimension lengths in declaration order (outermost first), or /empty for a + /// one-dimensional aggregate array. A single-element list is treated the same as . + /// + internal void SetAggregateElements(IReadOnlyList elements, IReadOnlyList dimensions = null) { m_AggregateElements = elements ?? throw new ArgumentNullException(nameof(elements)); + m_AggregateDimensions = dimensions != null && dimensions.Count > 1 ? dimensions : Array.Empty(); } /// /// Publishes a completed set of scalar element reads as one aggregate value and mirrors it into a compatible typed array tag. + /// A multidimensional aggregate array ( has more than one entry) is reshaped into a + /// genuine multidimensional instead of the concrete tag's inherently flat Value property. /// /// The first element error, or zero when every element succeeded. internal virtual void CompleteAggregateRead(ulong itemError) @@ -126,29 +176,39 @@ internal virtual void CompleteAggregateRead(ulong itemError) return; } - var values = Array.CreateInstance(elementValueProperty.PropertyType, m_AggregateElements.Count); + var flatValues = Array.CreateInstance(elementValueProperty.PropertyType, m_AggregateElements.Count); for (var index = 0; index < m_AggregateElements.Count; index++) { - values.SetValue(elementValueProperty.GetValue(m_AggregateElements[index]), index); + flatValues.SetValue(elementValueProperty.GetValue(m_AggregateElements[index]), index); } - AggregateValue = values; - var aggregateValueProperty = GetType().GetProperty("Value", BindingFlags.Instance | BindingFlags.Public); - if (aggregateValueProperty?.PropertyType == values.GetType()) + if (m_AggregateDimensions.Count > 1) + { + AggregateValue = ReshapeToMultiDimensional(elementValueProperty.PropertyType, flatValues, m_AggregateDimensions); + } + else { - aggregateValueProperty.SetValue(this, values); + AggregateValue = flatValues; + var aggregateValueProperty = GetType().GetProperty("Value", BindingFlags.Instance | BindingFlags.Public); + if (aggregateValueProperty?.PropertyType == flatValues.GetType()) + { + aggregateValueProperty.SetValue(this, flatValues); + } } Quality = PlcTagQC.TAG_QUALITY_GOOD; } /// /// Copies a typed parent array value into its scalar element tags before an aggregate write is encoded. - /// Unsupported native array types intentionally keep their explicitly populated element values unchanged. + /// Unsupported native array types intentionally keep their explicitly populated element values unchanged. A + /// multidimensional aggregate array is intentionally skipped here because already distributed + /// its values directly into at assignment time (its concrete tag's Value property + /// cannot hold a multidimensional shape). /// /// A typed parent array does not contain exactly one value per aggregate element. internal virtual void PrepareAggregateWrite() { - if (m_AggregateElements.Count == 0) + if (m_AggregateElements.Count == 0 || m_AggregateDimensions.Count > 1) { return; } @@ -177,6 +237,89 @@ internal virtual void PrepareAggregateWrite() } } + /// + /// Distributes a caller-supplied multidimensional array directly into the scalar element tags' Value properties, + /// in the same row-major order used by and the PLC's own element access IDs. + /// + /// The multidimensional array to distribute. Its shape must match . + /// The array's rank or per-dimension lengths do not match the PLC-declared dimensions. + private void DistributeMultiDimensionalValue(Array multiDimValue) + { + if (multiDimValue.Rank != m_AggregateDimensions.Count) + { + throw new ArgumentException( + $"Aggregate tag '{Name}' requires an array of rank {m_AggregateDimensions.Count}, but received rank {multiDimValue.Rank}.", + nameof(multiDimValue)); + } + for (var dimension = 0; dimension < m_AggregateDimensions.Count; dimension++) + { + if (multiDimValue.GetLength(dimension) != m_AggregateDimensions[dimension]) + { + throw new ArgumentException( + $"Aggregate tag '{Name}' requires dimension {dimension} to have length {m_AggregateDimensions[dimension]}, but received {multiDimValue.GetLength(dimension)}.", + nameof(multiDimValue)); + } + } + + var flatIndex = 0; + foreach (var indices in EnumerateRowMajorIndices(m_AggregateDimensions)) + { + var elementValueProperty = m_AggregateElements[flatIndex].GetType().GetProperty("Value", BindingFlags.Instance | BindingFlags.Public); + if (elementValueProperty == null) + { + throw new InvalidOperationException($"Aggregate element '{m_AggregateElements[flatIndex].Name}' has no writable Value property."); + } + elementValueProperty.SetValue(m_AggregateElements[flatIndex], multiDimValue.GetValue(indices)); + flatIndex++; + } + } + + /// + /// Reshapes a flat array, ordered exactly like , into a genuine multidimensional array. + /// + /// The CLR element type shared by every scalar element tag's Value property. + /// The flat values in row-major order (outermost dimension first). + /// The PLC-declared dimension lengths in declaration order (outermost first). + /// A multidimensional array of with one entry per element of . + private static Array ReshapeToMultiDimensional(Type elementType, Array flatValues, IReadOnlyList dimensions) + { + var lengths = dimensions.Select(dimension => checked((int)dimension)).ToArray(); + var multiDimValues = Array.CreateInstance(elementType, lengths); + var flatIndex = 0; + foreach (var indices in EnumerateRowMajorIndices(dimensions)) + { + multiDimValues.SetValue(flatValues.GetValue(flatIndex), indices); + flatIndex++; + } + return multiDimValues; + } + + /// + /// Enumerates every index combination of a multidimensional shape in row-major order (outermost dimension varies + /// slowest), matching the order in which the PLC protocol enumerates aggregate array element access IDs. + /// + /// The dimension lengths in declaration order (outermost first). + private static IEnumerable EnumerateRowMajorIndices(IReadOnlyList dimensions) + { + var indices = new int[dimensions.Count]; + var totalCount = 1L; + foreach (var dimension in dimensions) + { + totalCount = checked(totalCount * dimension); + } + + for (var flatIndex = 0L; flatIndex < totalCount; flatIndex++) + { + var remainder = flatIndex; + for (var dimension = dimensions.Count - 1; dimension >= 0; dimension--) + { + indices[dimension] = checked((int)(remainder % dimensions[dimension])); + remainder /= dimensions[dimension]; + } + yield return indices; + } + } + protected static string ResultString(PlcTag tag, string value ) { return String.Format("{0:X02}: {1}", tag.Quality, value); @@ -981,6 +1124,8 @@ public PlcTagString(string name, ItemAddress address, uint softdatatype, byte ma m_MaxLength = maxlength; } + internal override int GetMaxStringLength() => m_MaxLength; + public override void ProcessReadResult(object valueObj, ulong error) { LastReadError = error; @@ -1483,6 +1628,8 @@ public PlcTagWString(string name, ItemAddress address, uint softdatatype, ushort m_MaxLength = maxlength; } + internal override int GetMaxStringLength() => m_MaxLength; + public override void ProcessReadResult(object valueObj, ulong error) { LastReadError = error; @@ -2619,6 +2766,8 @@ public PlcTagStringArray(string name, ItemAddress address, uint softdatatype, by m_MaxLength = maxlength; } + internal override int GetMaxStringLength() => m_MaxLength; + public override void ProcessReadResult(object valueObj, ulong error) { LastReadError = error; diff --git a/src/S7CommPlusDriver/ClientApi/PlcTags.cs b/src/S7CommPlusDriver/ClientApi/PlcTags.cs index 09ad4e7..afd20a1 100644 --- a/src/S7CommPlusDriver/ClientApi/PlcTags.cs +++ b/src/S7CommPlusDriver/ClientApi/PlcTags.cs @@ -78,8 +78,13 @@ public static int WriteTags(this S7CommPlusProtocolSession conn, IEnumerable + /// + /// The PLC-declared maximum length for values, or zero when unknown. + /// Ignored for every other datatype. A value of zero falls back to the tag's built-in default of 254, matching the previous + /// behavior for callers that cannot supply this metadata. + /// /// The matching concrete tag implementation, or for an unsupported datatype. - public static PlcTag TagFactory(string name, ItemAddress address, uint softdatatype, bool Is1Dim = false) + public static PlcTag TagFactory(string name, ItemAddress address, uint softdatatype, bool Is1Dim = false, int maxStringLength = 0) { switch (softdatatype) { @@ -127,9 +132,15 @@ public static PlcTag TagFactory(string name, ItemAddress address, uint softdatat return new PlcTagDateAndTime(name, address, softdatatype); case Softdatatype.S7COMMP_SOFTDATATYPE_STRING: - if (Is1Dim) - return new PlcTagStringArray(name, address, softdatatype); - return new PlcTagString(name, address, softdatatype); + { + // A value of 0 (unknown/not supplied by the caller) keeps the tag's own built-in default (254). + var stringMaxLength = maxStringLength > 0 && maxStringLength <= byte.MaxValue + ? (byte)maxStringLength + : (byte)254; + if (Is1Dim) + return new PlcTagStringArray(name, address, softdatatype, stringMaxLength); + return new PlcTagString(name, address, softdatatype, stringMaxLength); + } case Softdatatype.S7COMMP_SOFTDATATYPE_POINTER: return new PlcTagPointer(name, address, softdatatype); @@ -178,7 +189,13 @@ public static PlcTag TagFactory(string name, ItemAddress address, uint softdatat case Softdatatype.S7COMMP_SOFTDATATYPE_WCHAR: return new PlcTagWChar(name, address, softdatatype); case Softdatatype.S7COMMP_SOFTDATATYPE_WSTRING: - return new PlcTagWString(name, address, softdatatype); + { + // A value of 0 (unknown/not supplied by the caller) keeps the tag's own built-in default (254). + var wstringMaxLength = maxStringLength > 0 && maxStringLength <= ushort.MaxValue + ? (ushort)maxStringLength + : (ushort)254; + return new PlcTagWString(name, address, softdatatype, wstringMaxLength); + } //case Softdatatype.S7COMMP_SOFTDATATYPE_VARIANT: //-> Variant isn't added inside of the instance-db as a variable! case Softdatatype.S7COMMP_SOFTDATATYPE_LTIME: diff --git a/src/S7CommPlusDriver/Core/POffsetInfoType.cs b/src/S7CommPlusDriver/Core/POffsetInfoType.cs index 1972a65..3eefd3f 100644 --- a/src/S7CommPlusDriver/Core/POffsetInfoType.cs +++ b/src/S7CommPlusDriver/Core/POffsetInfoType.cs @@ -69,6 +69,12 @@ public enum OffsetInfoType public abstract bool Is1Dim(); public abstract bool IsMDim(); + /// + /// Gets the maximum string length for a String/WString offset info, or 0 when not applicable. + /// + /// The maximum length in characters, or 0 for non-string offset info types. + public virtual int MaxStringLength() { return 0; } + public static POffsetInfoType Deserialize(Stream buffer, int offsetinfotype, out int length) { switch ((OffsetInfoType)offsetinfotype) @@ -542,6 +548,8 @@ public class POffsetInfoType_Array1Dim : POffsetInfoType, IOffsetInfoType_1Dim public override bool HasRelation() { return false; } public override bool Is1Dim() { return true; } public override bool IsMDim() { return false; } + // For a string array, UnspecifiedOffsetinfo1 carries the maximum string length of the array elements. + public override int MaxStringLength() { return UnspecifiedOffsetinfo1; } public static POffsetInfoType_Array1Dim Deserialize(Stream buffer, out int length) { @@ -599,6 +607,8 @@ public class POffsetInfoType_String: POffsetInfoType public override bool HasRelation() { return false; } public override bool Is1Dim() { return false; } public override bool IsMDim() { return false; } + // Some firmware versions swap UnspecifiedOffsetinfo1/2, so take the smaller value as the max. string length. + public override int MaxStringLength() { return Math.Min(UnspecifiedOffsetinfo1, UnspecifiedOffsetinfo2); } public static POffsetInfoType_String Deserialize(Stream buffer, out int length) { diff --git a/src/S7CommPlusDriver/OpenSSL/OpenSSLConnector.cs b/src/S7CommPlusDriver/OpenSSL/OpenSSLConnector.cs index da21d9a..5f513a4 100644 --- a/src/S7CommPlusDriver/OpenSSL/OpenSSLConnector.cs +++ b/src/S7CommPlusDriver/OpenSSL/OpenSSLConnector.cs @@ -33,6 +33,8 @@ public class OpenSSLConnector : IDisposable private readonly byte[] m_buffer = new byte[4096]; private readonly DataBufferList m_pendingWriteList; private readonly DataBufferList m_pendingReadList; + private readonly object m_readListLock = new object(); + private readonly object m_writeListLock = new object(); private bool m_fatalError; private bool m_disposed; @@ -277,8 +279,16 @@ protected void RunSSL() while (!m_fatalError && ((!m_readRequired && dataToWrite) || dataToRead)) { - int pendingReadBefore = m_pendingReadList.Count; - int pendingWriteBefore = m_pendingWriteList.Count; + int pendingReadBefore; + int pendingWriteBefore; + lock (m_readListLock) + { + pendingReadBefore = m_pendingReadList.Count; + } + lock (m_writeListLock) + { + pendingWriteBefore = m_pendingWriteList.Count; + } bool readRequiredBefore = m_readRequired; var bioPendingBefore = Native.BIO_ctrl_pending(m_pBioOut); @@ -304,8 +314,18 @@ protected void RunSSL() GetPendingOperations(ref dataToRead, ref dataToWrite); var bioPendingAfter = Native.BIO_ctrl_pending(m_pBioOut); - if (pendingReadBefore == m_pendingReadList.Count && - pendingWriteBefore == m_pendingWriteList.Count && + int pendingReadAfter; + int pendingWriteAfter; + lock (m_readListLock) + { + pendingReadAfter = m_pendingReadList.Count; + } + lock (m_writeListLock) + { + pendingWriteAfter = m_pendingWriteList.Count; + } + if (pendingReadBefore == pendingReadAfter && + pendingWriteBefore == pendingWriteAfter && readRequiredBefore == m_readRequired && bioPendingBefore == bioPendingAfter) { @@ -341,8 +361,14 @@ public void ReadCompleted(byte[] pData, int dataLen) private void GetPendingOperations(ref bool dataToRead, ref bool dataToWrite) { - dataToRead = m_pendingReadList.Count > 0; - dataToWrite = m_pendingWriteList.Count > 0; + lock (m_readListLock) + { + dataToRead = m_pendingReadList.Count > 0; + } + lock (m_writeListLock) + { + dataToWrite = m_pendingWriteList.Count > 0; + } } private void PerformRead() @@ -437,23 +463,35 @@ private void UseData(DataBufferList list, DataBuffer pBuffer, int result) private void AppendBuffer(DataBufferList list, DataBuffer pBuffer) { - list.AddLast(pBuffer); + object lockObject = (list == m_pendingReadList) ? m_readListLock : m_writeListLock; + lock (lockObject) + { + list.AddLast(pBuffer); + } } DataBuffer GetNextBuffer(DataBufferList list) { DataBuffer head = null; - if (list.Count > 0) + object lockObject = (list == m_pendingReadList) ? m_readListLock : m_writeListLock; + lock (lockObject) { - head = list.First.Value; - list.RemoveFirst(); + if (list.Count > 0) + { + head = list.First.Value; + list.RemoveFirst(); + } } return head; } void PutbackBuffer(DataBufferList list, DataBuffer pBuffer) { - list.AddFirst(pBuffer); + object lockObject = (list == m_pendingReadList) ? m_readListLock : m_writeListLock; + lock (lockObject) + { + list.AddFirst(pBuffer); + } } /// diff --git a/src/S7CommPlusDriver/S7CommPlusProtocolSession.cs b/src/S7CommPlusDriver/S7CommPlusProtocolSession.cs index 58e8b11..c06a260 100644 --- a/src/S7CommPlusDriver/S7CommPlusProtocolSession.cs +++ b/src/S7CommPlusDriver/S7CommPlusProtocolSession.cs @@ -2067,7 +2067,8 @@ internal static bool IsAggregatePrimitiveArray(PVartypeListElement varType, stri private static PlcTag CreateResolvedPlcTag(VarInfo varInfo, PVartypeListElement varType, bool isAggregateArray) { var address = CreateItemAddress(varInfo); - var tag = PlcTags.TagFactory(varInfo.Name, address, varType.Softdatatype, isAggregateArray); + var maxStringLength = varType.OffsetInfoType?.MaxStringLength() ?? 0; + var tag = PlcTags.TagFactory(varInfo.Name, address, varType.Softdatatype, isAggregateArray, maxStringLength); tag?.SetTraceAddressMetadata(varInfo); if (!isAggregateArray || tag == null) { @@ -2081,14 +2082,34 @@ private static PlcTag CreateResolvedPlcTag(VarInfo varInfo, PVartypeListElement varInfo.AccessSequence, accessId, varType.OffsetInfoType.HasRelation()); - return PlcTags.TagFactory($"{varInfo.Name}[#{accessId}]", elementAddress, varType.Softdatatype); + return PlcTags.TagFactory($"{varInfo.Name}[#{accessId}]", elementAddress, varType.Softdatatype, false, maxStringLength); }) .Where(elementTag => elementTag != null) .ToList(); - tag.SetAggregateElements(elementTags); + tag.SetAggregateElements(elementTags, GetAggregateArrayDimensions(varType)); return tag; } + /// + /// Extracts the PLC-declared dimension lengths (outermost first) of an aggregate primitive array, for reshaping the + /// flat element-tag list into a genuine multidimensional array. Returns an empty list for one-dimensional arrays. + /// + /// The resolved member datatype and dimension metadata. + private static IReadOnlyList GetAggregateArrayDimensions(PVartypeListElement varType) + { + if (varType.OffsetInfoType is not IOffsetInfoType_MDim multiDimensional) + { + return Array.Empty(); + } + + // GetMdimArrayElementCount() stores dimensions inside-out; reverse to declaration order (outermost first) and + // drop trailing zero-length entries, matching GetAggregateArrayElementAccessIds's own filtering. + return multiDimensional.GetMdimArrayElementCount() + .TakeWhile(count => count > 0) + .Reverse() + .ToArray(); + } + /// /// Creates a typed tag directly from one aggregate browse result, avoiding a second walk through the PLC type catalog. /// @@ -2106,7 +2127,7 @@ internal static PlcTag CreateResolvedPlcTag(VarInfo varInfo) var address = CreateItemAddress(varInfo); var isAggregateArray = varInfo.ArrayElementCount > 0; - var tag = PlcTags.TagFactory(varInfo.Name, address, varInfo.Softdatatype, isAggregateArray); + var tag = PlcTags.TagFactory(varInfo.Name, address, varInfo.Softdatatype, isAggregateArray, varInfo.MaxStringLength); tag?.SetTraceAddressMetadata(varInfo); if (!isAggregateArray || tag == null) { @@ -2120,10 +2141,15 @@ internal static PlcTag CreateResolvedPlcTag(VarInfo varInfo) varInfo.AccessSequence, accessId, RequiresArrayElementRelationSelector(varInfo.Softdatatype)), - varInfo.Softdatatype)) + varInfo.Softdatatype, + false, + varInfo.MaxStringLength)) .Where(elementTag => elementTag != null) .ToList(); - tag.SetAggregateElements(elementTags); + var dimensions = varInfo.ArrayDimensions != null && varInfo.ArrayDimensions.Count > 1 + ? varInfo.ArrayDimensions.Select(dimension => dimension.ElementCount).ToArray() + : Array.Empty(); + tag.SetAggregateElements(elementTags, dimensions); return tag; } @@ -2321,7 +2347,9 @@ internal static bool TryCreateResolvedPrimitiveArrayElement( aggregateVariable.AccessSequence, accessId, RequiresArrayElementRelationSelector(aggregateVariable.Softdatatype)), - aggregateVariable.Softdatatype); + aggregateVariable.Softdatatype, + false, + aggregateVariable.MaxStringLength); return tag != null; } diff --git a/src/S7CommPlusDriver/S7CommPlusTagAccessorCatalog.cs b/src/S7CommPlusDriver/S7CommPlusTagAccessorCatalog.cs index 7658b5f..4a4d310 100644 --- a/src/S7CommPlusDriver/S7CommPlusTagAccessorCatalog.cs +++ b/src/S7CommPlusDriver/S7CommPlusTagAccessorCatalog.cs @@ -19,7 +19,7 @@ namespace S7CommPlusDriver /// public sealed class S7CommPlusTagAccessorCatalog { - private const int CurrentFormatVersion = 2; + private const int CurrentFormatVersion = 3; private const int MinimumSupportedFormatVersion = 1; private const int MaximumEntryCount = 2_000_000; private const int MaximumAggregateElementCount = 2_000_000; @@ -238,7 +238,7 @@ public static S7CommPlusTagAccessorCatalog ReadFrom(Stream source, string expect entries.Add( symbol, reader.ReadBoolean() - ? TagDescriptor.ReadFrom(reader, 0, version >= 2 ? symbol : null) + ? TagDescriptor.ReadFrom(reader, 0, version, version >= 2 ? symbol : null) : null); } return new S7CommPlusTagAccessorCatalog(structureHash, entries); @@ -329,16 +329,21 @@ private sealed class TagDescriptor /// The Siemens soft-datatype identifier. /// The immutable item-address descriptor. /// Scalar elements used to transfer an aggregate array. + /// + /// The PLC-declared maximum string length for String/WString tags, or zero for every other datatype. + /// private TagDescriptor( string name, uint datatype, AddressDescriptor address, - IReadOnlyList aggregateElements) + IReadOnlyList aggregateElements, + int maxStringLength) { Name = name; Datatype = datatype; _address = address; _aggregateElements = aggregateElements; + MaxStringLength = maxStringLength; } /// Gets the symbolic or generated element name. @@ -347,6 +352,9 @@ private TagDescriptor( /// Gets the Siemens soft-datatype identifier. private uint Datatype { get; } + /// Gets the PLC-declared maximum string length, or zero for every non-string datatype. + private int MaxStringLength { get; } + /// Creates a detached immutable descriptor from a resolved mutable driver tag. /// The resolved source tag. /// A recursively captured descriptor. @@ -357,14 +365,15 @@ internal static TagDescriptor FromTag(PlcTag tag) tag.Name, tag.Datatype, AddressDescriptor.FromAddress(tag.Address), - tag.AggregateElements.Select(FromTag).ToArray()); + tag.AggregateElements.Select(FromTag).ToArray(), + tag.GetMaxStringLength()); } /// Creates a fresh mutable driver tag and recursively recreates aggregate element tags. /// An accessor that shares no mutable state with previous materializations. internal PlcTag CreateTag(string nameOverride = null) { - var tag = PlcTags.TagFactory(nameOverride ?? Name, _address.CreateAddress(), Datatype, _aggregateElements.Count > 0); + var tag = PlcTags.TagFactory(nameOverride ?? Name, _address.CreateAddress(), Datatype, _aggregateElements.Count > 0, MaxStringLength); if (tag == null) { throw new InvalidDataException($"The cached datatype {Datatype} for symbol '{Name}' is not supported by this driver version."); @@ -391,13 +400,18 @@ internal void WriteTo(BinaryWriter writer, bool includeName = true) { element.WriteTo(writer); } + writer.Write(MaxStringLength); } /// Reads and validates one recursive descriptor from the catalog stream. /// The source reader. /// The current nesting depth used to reject maliciously recursive cache files. + /// + /// The catalog format version being read. Versions below 3 () predate persisted + /// string-length metadata; such caches fall back to 0, which recreates the tag's built-in default (254). + /// /// The validated descriptor. - internal static TagDescriptor ReadFrom(BinaryReader reader, int depth, string nameOverride = null) + internal static TagDescriptor ReadFrom(BinaryReader reader, int depth, int formatVersion, string nameOverride = null) { if (depth > MaximumDescriptorDepth) { @@ -414,9 +428,10 @@ internal static TagDescriptor ReadFrom(BinaryReader reader, int depth, string na var elements = new TagDescriptor[elementCount]; for (var index = 0; index < elementCount; index++) { - elements[index] = ReadFrom(reader, depth + 1); + elements[index] = ReadFrom(reader, depth + 1, formatVersion); } - return new TagDescriptor(name, datatype, address, elements); + var maxStringLength = formatVersion >= 3 ? reader.ReadInt32() : 0; + return new TagDescriptor(name, datatype, address, elements, maxStringLength); } }