Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
268 changes: 268 additions & 0 deletions src/S7CommPlusDriver.Tests/PlcTagMultiDimensionalArrayTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
using System;
using System.Linq;
using S7CommPlusDriver.ClientApi;
using Xunit;

namespace S7CommPlusDriver.Tests
{
/// <summary>
/// Verifies that a multidimensional primitive PLC array (e.g. <c>Array[1..2, 1..3] of Int</c>) 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.
/// </summary>
/// <remarks>
/// Regression test for a design gap ported from the upstream fork's <c>e91d8f2</c> ("Extended DataTypes") commit, which
/// introduced dedicated <c>PlcTagXxxArrayMDim</c> types there. This driver instead extends the existing
/// aggregate-element mechanism (<see cref="PlcTag.SetAggregateElements"/>, <see cref="PlcTag.CompleteAggregateRead"/>,
/// <see cref="PlcTag.SetValue"/>) to reshape/distribute values without introducing new tag types.
/// </remarks>
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<short[,]>(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<short[]>(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<bool[,]>(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<float[,]>(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<string[,]>(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<DateTime[,]>(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;
}
}
}
91 changes: 91 additions & 0 deletions src/S7CommPlusDriver.Tests/PlcTagStringMaxLengthTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using S7CommPlusDriver.ClientApi;
using Xunit;

namespace S7CommPlusDriver.Tests
{
/// <summary>
/// Verifies that <see cref="PlcTags.TagFactory"/> forwards the PLC-declared maximum string length to
/// <see cref="PlcTagString"/>, <see cref="PlcTagStringArray"/>, and <see cref="PlcTagWString"/>, and that the resulting
/// write telegram matches the declared length instead of always falling back to the built-in default of 254.
/// </summary>
/// <remarks>
/// Regression test for a bug where <c>TagFactory</c> never received the declared maximum string length, so every
/// <c>String</c>/<c>WString</c> tag was created with the built-in default of 254, regardless of its actual PLC
/// declaration (e.g. <c>String[10]</c>). Writing such a tag then sent a telegram of the wrong size and with the wrong
/// declared-length header byte, which the PLC rejected.
/// </remarks>
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<PlcTagString>(tag);
stringTag.Value = "0123456789"; // exactly 10 characters, must not throw

var writeValue = Assert.IsType<ValueUSIntArray>(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<PlcTagString>(tag);
stringTag.Value = "Hello";

var writeValue = Assert.IsType<ValueUSIntArray>(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<PlcTagStringArray>(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<PlcTagWString>(tag);
Assert.Equal(20, wstringTag.GetMaxStringLength());
}

private static PValue GetWriteValue(PlcTag tag)
{
return tag.GetWriteValue();
}
}
}
7 changes: 7 additions & 0 deletions src/S7CommPlusDriver/ClientApi/Browser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -940,6 +941,12 @@ public class VarInfo
/// </summary>
public IReadOnlyList<S7CommPlusArrayDimension> ArrayDimensions = Array.Empty<S7CommPlusArrayDimension>();

/// <summary>
/// Gets the maximum string length declared in the PLC for <c>String</c>/<c>WString</c> values, or zero for every
/// other datatype. Required to build a correctly sized write telegram; see <see cref="POffsetInfoType.MaxStringLength"/>.
/// </summary>
public int MaxStringLength;

/// <summary>Retains the parsed symbol path so tag creation can reuse the exact CRC inputs.</summary>
internal List<S7CommPlusSymbolCrc.PathSegment> SymbolCrcPath;

Expand Down
Loading
Loading