diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0d13810..ad5d9a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,14 +14,14 @@ jobs: DOTNET_NOLOGO : 1 strategy: matrix: - os: [windows-latest, ubuntu-latest, macos-latest] + os: [windows-latest, ubuntu-20.04, macos-latest] test-framework: [netcoreapp3.1, net5.0] include: - - os: ubuntu-latest + - os: ubuntu-20.04 test-framework: netcoreapp3.1 installSnap7: true dotnet-sdk: '3.1.x' - - os: ubuntu-latest + - os: ubuntu-20.04 test-framework: net5.0 installSnap7: true dotnet-sdk: '5.0.x' @@ -48,7 +48,7 @@ jobs: - uses: actions/checkout@v2 - name: Install Snap7 Linux - if: ${{ matrix.installSnap7 && matrix.os == 'ubuntu-latest' }} + if: ${{ matrix.installSnap7 && matrix.os == 'ubuntu-20.04' }} run: | sudo add-apt-repository ppa:gijzelaar/snap7 sudo apt-get update diff --git a/S7.Net.UnitTest/Helpers/TestClass.cs b/S7.Net.UnitTest/Helpers/TestClass.cs index c810adc..2f2c775 100644 --- a/S7.Net.UnitTest/Helpers/TestClass.cs +++ b/S7.Net.UnitTest/Helpers/TestClass.cs @@ -1,4 +1,6 @@  +using S7.Net.Types; + namespace S7.Net.UnitTest.Helpers { class TestClass @@ -51,5 +53,16 @@ namespace S7.Net.UnitTest.Helpers /// DB1.DBD16 /// public ushort DWordVariable { get; set; } + + /// + /// DB1.DBX20.0 + /// + [S7String(S7StringType.S7WString, 10)] + public string WStringVariable { get; set; } + /// + /// DB1.DBX44.0 + /// + [S7String(S7StringType.S7String, 10)] + public string StringVariable { get; set; } } } diff --git a/S7.Net.UnitTest/S7NetTestsAsync.cs b/S7.Net.UnitTest/S7NetTestsAsync.cs index f94ddb8..731e802 100644 --- a/S7.Net.UnitTest/S7NetTestsAsync.cs +++ b/S7.Net.UnitTest/S7NetTestsAsync.cs @@ -7,6 +7,7 @@ using S7.Net.Types; using S7.UnitTest.Helpers; using System.Threading.Tasks; using System.Threading; +using System.Security.Cryptography; #endregion @@ -154,7 +155,9 @@ namespace S7.Net.UnitTest IntVariable = -15000, LRealVariable = -154.789, RealVariable = -154.789f, - DWordVariable = 850 + DWordVariable = 850, + WStringVariable = "ÄÜÉÊéà", + StringVariable = "Hallo" }; await plc.WriteClassAsync(tc, DB2); @@ -168,6 +171,8 @@ namespace S7.Net.UnitTest Assert.AreEqual(tc.LRealVariable, tc2.LRealVariable); Assert.AreEqual(tc.RealVariable, tc2.RealVariable); Assert.AreEqual(tc.DWordVariable, tc2.DWordVariable); + Assert.AreEqual(tc.WStringVariable, tc2.WStringVariable); + Assert.AreEqual(tc.StringVariable, tc2.StringVariable); } [TestMethod] @@ -580,7 +585,9 @@ namespace S7.Net.UnitTest IntVariable = -15000, LRealVariable = -154.789, RealVariable = -154.789f, - DWordVariable = 850 + DWordVariable = 850, + WStringVariable = "ÄÜÉÊéà", + StringVariable = "Hallo" }; await plc.WriteClassAsync(tc, DB2); @@ -628,7 +635,10 @@ namespace S7.Net.UnitTest IntVariable = -15000, LRealVariable = -154.789, RealVariable = -154.789f, - DWordVariable = 850 + DWordVariable = 850, + WStringVariable = "ÄÜÉÊéà", + StringVariable = "Hallo" + }; await plc.WriteClassAsync(tc, DB2); @@ -646,6 +656,9 @@ namespace S7.Net.UnitTest Assert.AreEqual(Math.Round(tc2.LRealVariable, 3), Math.Round(tc2Generic.LRealVariable, 3)); Assert.AreEqual(tc2.RealVariable, tc2Generic.RealVariable); Assert.AreEqual(tc2.DWordVariable, tc2Generic.DWordVariable); + Assert.AreEqual(tc2.WStringVariable, tc2Generic.WStringVariable); + Assert.AreEqual(tc2.StringVariable, tc2Generic.StringVariable); + } [TestMethod] @@ -671,7 +684,9 @@ namespace S7.Net.UnitTest IntVariable = -15000, LRealVariable = -154.789, RealVariable = -154.789f, - DWordVariable = 850 + DWordVariable = 850, + WStringVariable = "ÄÜÉÊéà", + StringVariable = "Hallo" }; await plc.WriteClassAsync(tc, DB2); @@ -686,6 +701,8 @@ namespace S7.Net.UnitTest Assert.AreEqual(Math.Round(tc2Generic.LRealVariable, 3), Math.Round(tc2GenericWithClassFactory.LRealVariable, 3)); Assert.AreEqual(tc2Generic.RealVariable, tc2GenericWithClassFactory.RealVariable); Assert.AreEqual(tc2Generic.DWordVariable, tc2GenericWithClassFactory.DWordVariable); + Assert.AreEqual(tc2Generic.WStringVariable, tc2GenericWithClassFactory.WStringVariable); + Assert.AreEqual(tc2Generic.StringVariable, tc2GenericWithClassFactory.StringVariable); } [TestMethod] @@ -792,7 +809,9 @@ namespace S7.Net.UnitTest IntVariable = -15000, LRealVariable = -154.789, RealVariable = -154.789f, - DWordVariable = 850 + DWordVariable = 850, + WStringVariable = "ÄÜÉÊéà", + StringVariable = "Hallo" }; plc.WriteClass(tc, DB2); diff --git a/S7.Net.UnitTest/S7NetTestsSync.cs b/S7.Net.UnitTest/S7NetTestsSync.cs index c84c1a7..76317c0 100644 --- a/S7.Net.UnitTest/S7NetTestsSync.cs +++ b/S7.Net.UnitTest/S7NetTestsSync.cs @@ -5,6 +5,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using S7.Net.UnitTest.Helpers; using S7.Net.Types; using S7.UnitTest.Helpers; +using System.Security.Cryptography; #endregion @@ -183,6 +184,9 @@ namespace S7.Net.UnitTest tc.LRealVariable = -154.789; tc.RealVariable = -154.789f; tc.DWordVariable = 850; + tc.WStringVariable = "ÄÜÉÊéà"; + tc.StringVariable = "Hallo"; + plc.WriteClass(tc, DB2); TestClass tc2 = new TestClass(); // Values that are read from a class are stored inside the class itself, that is passed by reference @@ -194,6 +198,8 @@ namespace S7.Net.UnitTest Assert.AreEqual(tc.LRealVariable, tc2.LRealVariable); Assert.AreEqual(tc.RealVariable, tc2.RealVariable); Assert.AreEqual(tc.DWordVariable, tc2.DWordVariable); + Assert.AreEqual(tc.WStringVariable, tc2.WStringVariable); + Assert.AreEqual(tc.StringVariable, tc2.StringVariable); } /// @@ -577,6 +583,8 @@ namespace S7.Net.UnitTest tc.LRealVariable = -154.789; tc.RealVariable = -154.789f; tc.DWordVariable = 850; + tc.WStringVariable = "ÄÜÉÊéà"; + tc.StringVariable = "Hallo"; plc.WriteClass(tc, DB2); @@ -622,6 +630,8 @@ namespace S7.Net.UnitTest tc.LRealVariable = -154.789; tc.RealVariable = -154.789f; tc.DWordVariable = 850; + tc.WStringVariable = "ÄÜÉÊéà"; + tc.StringVariable = "Hallo"; plc.WriteClass(tc, DB2); @@ -637,6 +647,8 @@ namespace S7.Net.UnitTest Assert.AreEqual(Math.Round(tc2.LRealVariable, 3), Math.Round(tc2Generic.LRealVariable, 3)); Assert.AreEqual(tc2.RealVariable, tc2Generic.RealVariable); Assert.AreEqual(tc2.DWordVariable, tc2Generic.DWordVariable); + Assert.AreEqual(tc2.WStringVariable, tc2Generic.WStringVariable); + Assert.AreEqual(tc2.StringVariable, tc2Generic.StringVariable); } [TestMethod, ExpectedException(typeof(PlcException))] @@ -665,6 +677,8 @@ namespace S7.Net.UnitTest tc.LRealVariable = -154.789; tc.RealVariable = -154.789f; tc.DWordVariable = 850; + tc.WStringVariable = "ÄÜÉÊéà"; + tc.StringVariable = "Hallo"; plc.WriteClass(tc, DB2); @@ -679,6 +693,8 @@ namespace S7.Net.UnitTest Assert.AreEqual(Math.Round(tc2Generic.LRealVariable, 3), Math.Round(tc2GenericWithClassFactory.LRealVariable, 3)); Assert.AreEqual(tc2Generic.RealVariable, tc2GenericWithClassFactory.RealVariable); Assert.AreEqual(tc2Generic.DWordVariable, tc2GenericWithClassFactory.DWordVariable); + Assert.AreEqual(tc2Generic.WStringVariable, tc2GenericWithClassFactory.WStringVariable); + Assert.AreEqual(tc2Generic.StringVariable, tc2GenericWithClassFactory.StringVariable); } [TestMethod, ExpectedException(typeof(PlcException))] @@ -837,6 +853,9 @@ namespace S7.Net.UnitTest tc.LRealVariable = -154.789; tc.RealVariable = -154.789f; tc.DWordVariable = 850; + tc.WStringVariable = "ÄÜÉÊéà"; + tc.StringVariable = "Hallo"; + plc.WriteClass(tc, DB2); int expectedReadBytes = (int)Types.Class.GetClassSize(tc); diff --git a/S7.Net/PlcAsynchronous.cs b/S7.Net/PlcAsynchronous.cs index a58f629..f75cf23 100644 --- a/S7.Net/PlcAsynchronous.cs +++ b/S7.Net/PlcAsynchronous.cs @@ -25,7 +25,7 @@ namespace S7.Net /// A task that represents the asynchronous open operation. public async Task OpenAsync(CancellationToken cancellationToken = default) { - var stream = await ConnectAsync().ConfigureAwait(false); + var stream = await ConnectAsync(cancellationToken).ConfigureAwait(false); try { await queue.Enqueue(async () => @@ -44,11 +44,16 @@ namespace S7.Net } } - private async Task ConnectAsync() + private async Task ConnectAsync(CancellationToken cancellationToken) { tcpClient = new TcpClient(); ConfigureConnection(); + +#if NET5_0_OR_GREATER + await tcpClient.ConnectAsync(IP, Port, cancellationToken).ConfigureAwait(false); +#else await tcpClient.ConnectAsync(IP, Port).ConfigureAwait(false); +#endif return tcpClient.GetStream(); } diff --git a/S7.Net/S7.Net.csproj b/S7.Net/S7.Net.csproj index 85ef368..fa85b08 100644 --- a/S7.Net/S7.Net.csproj +++ b/S7.Net/S7.Net.csproj @@ -1,7 +1,7 @@  - net452;netstandard2.0;netstandard1.3 + net452;netstandard2.0;netstandard1.3;net5.0 true Properties\S7.Net.snk S7.Net.UnitTest diff --git a/S7.Net/Types/Class.cs b/S7.Net/Types/Class.cs index 225f3f8..819b626 100644 --- a/S7.Net/Types/Class.cs +++ b/S7.Net/Types/Class.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; namespace S7.Net.Types { @@ -25,7 +26,7 @@ namespace S7.Net.Types } - private static double GetIncreasedNumberOfBytes(double numBytes, Type type) + private static double GetIncreasedNumberOfBytes(double numBytes, Type type, PropertyInfo? propertyInfo) { switch (type.Name) { @@ -38,30 +39,30 @@ namespace S7.Net.Types break; case "Int16": case "UInt16": - numBytes = Math.Ceiling(numBytes); - if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) - numBytes++; + IncrementToEven(ref numBytes); numBytes += 2; break; case "Int32": case "UInt32": - numBytes = Math.Ceiling(numBytes); - if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) - numBytes++; + IncrementToEven(ref numBytes); numBytes += 4; break; case "Single": - numBytes = Math.Ceiling(numBytes); - if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) - numBytes++; + IncrementToEven(ref numBytes); numBytes += 4; break; case "Double": - numBytes = Math.Ceiling(numBytes); - if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) - numBytes++; + IncrementToEven(ref numBytes); numBytes += 8; break; + case "String": + S7StringAttribute? attribute = propertyInfo?.GetCustomAttributes().SingleOrDefault(); + if (attribute == default(S7StringAttribute)) + throw new ArgumentException("Please add S7StringAttribute to the string field"); + + IncrementToEven(ref numBytes); + numBytes += attribute.ReservedLengthInBytes; + break; default: var propertyClass = Activator.CreateInstance(type); numBytes = GetClassSize(propertyClass, numBytes, true); @@ -93,12 +94,12 @@ namespace S7.Net.Types IncrementToEven(ref numBytes); for (int i = 0; i < array.Length; i++) { - numBytes = GetIncreasedNumberOfBytes(numBytes, elementType); + numBytes = GetIncreasedNumberOfBytes(numBytes, elementType, property); } } else { - numBytes = GetIncreasedNumberOfBytes(numBytes, property.PropertyType); + numBytes = GetIncreasedNumberOfBytes(numBytes, property.PropertyType, property); } } if (false == isInnerProperty) @@ -111,7 +112,7 @@ namespace S7.Net.Types return numBytes; } - private static object? GetPropertyValue(Type propertyType, byte[] bytes, ref double numBytes) + private static object? GetPropertyValue(Type propertyType, PropertyInfo? propertyInfo, byte[] bytes, ref double numBytes) { object? value = null; @@ -133,26 +134,20 @@ namespace S7.Net.Types numBytes++; break; case "Int16": - numBytes = Math.Ceiling(numBytes); - if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) - numBytes++; + IncrementToEven(ref numBytes); // hier auswerten ushort source = Word.FromBytes(bytes[(int)numBytes + 1], bytes[(int)numBytes]); value = source.ConvertToShort(); numBytes += 2; break; case "UInt16": - numBytes = Math.Ceiling(numBytes); - if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) - numBytes++; + IncrementToEven(ref numBytes); // hier auswerten value = Word.FromBytes(bytes[(int)numBytes + 1], bytes[(int)numBytes]); numBytes += 2; break; case "Int32": - numBytes = Math.Ceiling(numBytes); - if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) - numBytes++; + IncrementToEven(ref numBytes); var wordBuffer = new byte[4]; Array.Copy(bytes, (int)numBytes, wordBuffer, 0, wordBuffer.Length); uint sourceUInt = DWord.FromByteArray(wordBuffer); @@ -160,18 +155,14 @@ namespace S7.Net.Types numBytes += 4; break; case "UInt32": - numBytes = Math.Ceiling(numBytes); - if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) - numBytes++; + IncrementToEven(ref numBytes); var wordBuffer2 = new byte[4]; Array.Copy(bytes, (int)numBytes, wordBuffer2, 0, wordBuffer2.Length); value = DWord.FromByteArray(wordBuffer2); numBytes += 4; break; case "Single": - numBytes = Math.Ceiling(numBytes); - if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) - numBytes++; + IncrementToEven(ref numBytes); // hier auswerten value = Real.FromByteArray( new byte[] { @@ -182,15 +173,31 @@ namespace S7.Net.Types numBytes += 4; break; case "Double": - numBytes = Math.Ceiling(numBytes); - if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) - numBytes++; + IncrementToEven(ref numBytes); var buffer = new byte[8]; Array.Copy(bytes, (int)numBytes, buffer, 0, 8); // hier auswerten value = LReal.FromByteArray(buffer); numBytes += 8; break; + case "String": + S7StringAttribute? attribute = propertyInfo?.GetCustomAttributes().SingleOrDefault(); + if (attribute == default(S7StringAttribute)) + throw new ArgumentException("Please add S7StringAttribute to the string field"); + + IncrementToEven(ref numBytes); + + // get the value + var sData = new byte[attribute.ReservedLengthInBytes]; + Array.Copy(bytes, (int)numBytes, sData, 0, sData.Length); + value = attribute.Type switch + { + S7StringType.S7String => S7String.FromByteArray(sData), + S7StringType.S7WString => S7WString.FromByteArray(sData), + _ => throw new ArgumentException("Please use a valid string type for the S7StringAttribute") + }; + numBytes += sData.Length; + break; default: var propClass = Activator.CreateInstance(propertyType); numBytes = FromBytes(propClass, bytes, numBytes); @@ -222,7 +229,7 @@ namespace S7.Net.Types for (int i = 0; i < array.Length && numBytes < bytes.Length; i++) { array.SetValue( - GetPropertyValue(elementType, bytes, ref numBytes), + GetPropertyValue(elementType, property, bytes, ref numBytes), i); } } @@ -230,7 +237,7 @@ namespace S7.Net.Types { property.SetValue( sourceClass, - GetPropertyValue(property.PropertyType, bytes, ref numBytes), + GetPropertyValue(property.PropertyType, property, bytes, ref numBytes), null); } } @@ -238,7 +245,7 @@ namespace S7.Net.Types return numBytes; } - private static double SetBytesFromProperty(object propertyValue, byte[] bytes, double numBytes) + private static double SetBytesFromProperty(object propertyValue, PropertyInfo? propertyInfo, byte[] bytes, double numBytes) { int bytePos = 0; int bitPos = 0; @@ -280,6 +287,18 @@ namespace S7.Net.Types case "Double": bytes2 = LReal.ToByteArray((double)propertyValue); break; + case "String": + S7StringAttribute? attribute = propertyInfo?.GetCustomAttributes().SingleOrDefault(); + if (attribute == default(S7StringAttribute)) + throw new ArgumentException("Please add S7StringAttribute to the string field"); + + bytes2 = attribute.Type switch + { + S7StringType.S7String => S7String.ToByteArray((string)propertyValue, attribute.ReservedLength), + S7StringType.S7WString => S7WString.ToByteArray((string)propertyValue, attribute.ReservedLength), + _ => throw new ArgumentException("Please use a valid string type for the S7StringAttribute") + }; + break; default: numBytes = ToBytes(propertyValue, bytes, numBytes); break; @@ -315,12 +334,12 @@ namespace S7.Net.Types Type elementType = property.PropertyType.GetElementType(); for (int i = 0; i < array.Length && numBytes < bytes.Length; i++) { - numBytes = SetBytesFromProperty(array.GetValue(i), bytes, numBytes); + numBytes = SetBytesFromProperty(array.GetValue(i), property, bytes, numBytes); } } else { - numBytes = SetBytesFromProperty(property.GetValue(sourceClass, null), bytes, numBytes); + numBytes = SetBytesFromProperty(property.GetValue(sourceClass, null), property, bytes, numBytes); } } return numBytes; diff --git a/S7.Net/Types/S7String.cs b/S7.Net/Types/S7String.cs index 5bc383e..46c4808 100644 --- a/S7.Net/Types/S7String.cs +++ b/S7.Net/Types/S7String.cs @@ -8,7 +8,19 @@ namespace S7.Net.Types /// An S7 String has a preceeding 2 byte header containing its capacity and length /// public static class S7String - { + { + private static Encoding stringEncoding = Encoding.ASCII; + + /// + /// The Encoding used when serializing and deserializing S7String (Encoding.ASCII by default) + /// + /// StringEncoding must not be null + public static Encoding StringEncoding + { + get => stringEncoding; + set => stringEncoding = value ?? throw new ArgumentNullException(nameof(StringEncoding)); + } + /// /// Converts S7 bytes to a string /// @@ -30,7 +42,7 @@ namespace S7.Net.Types try { - return Encoding.ASCII.GetString(bytes, 2, length); + return StringEncoding.GetString(bytes, 2, length); } catch (Exception e) { @@ -38,7 +50,6 @@ namespace S7.Net.Types $"Failed to parse {VarType.S7String} from data. Following fields were read: size: '{size}', actual length: '{length}', total number of bytes (including header): '{bytes.Length}'.", e); } - } /// @@ -56,7 +67,7 @@ namespace S7.Net.Types if (reservedLength > 254) throw new ArgumentException($"The maximum string length supported is 254."); - var bytes = Encoding.ASCII.GetBytes(value); + var bytes = StringEncoding.GetBytes(value); if (bytes.Length > reservedLength) throw new ArgumentException($"The provided string length ({bytes.Length} is larger than the specified reserved length ({reservedLength})."); var buffer = new byte[2 + reservedLength]; diff --git a/S7.Net/Types/S7StringAttribute.cs b/S7.Net/Types/S7StringAttribute.cs index 4d6e107..768667d 100644 --- a/S7.Net/Types/S7StringAttribute.cs +++ b/S7.Net/Types/S7StringAttribute.cs @@ -2,7 +2,7 @@ namespace S7.Net.Types { - [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)] public sealed class S7StringAttribute : Attribute { private readonly S7StringType type; diff --git a/appveyor.yml b/appveyor.yml index 3e3100d..28da709 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -image: Visual Studio 2019 +image: Visual Studio 2022 configuration: Release install: - choco install gitversion.portable -y