From 0d2817661e0f876ec3ef9aa8be7000be023cefe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20P=C4=85gowski?= Date: Wed, 19 Jul 2023 20:30:32 +0200 Subject: [PATCH 1/4] Add S7 Time type (C# TimeSpan) Adds the S7 TIME (IEC) type (32 bits long) It is deserialized to C# TimeSpan and serialized as S7 DInt. --- S7.Net.UnitTest/S7NetTestsAsync.cs | 4 +- S7.Net/Enums.cs | 7 ++- S7.Net/PLCHelpers.cs | 10 ++++ S7.Net/Types/TimeSpan.cs | 95 ++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 S7.Net/Types/TimeSpan.cs diff --git a/S7.Net.UnitTest/S7NetTestsAsync.cs b/S7.Net.UnitTest/S7NetTestsAsync.cs index 5d598824..20338845 100644 --- a/S7.Net.UnitTest/S7NetTestsAsync.cs +++ b/S7.Net.UnitTest/S7NetTestsAsync.cs @@ -1006,7 +1006,7 @@ public async Task Test_Async_WriteLargeByteArrayWithCancellation() var db = 2; randomEngine.NextBytes(data); - cancellationSource.CancelAfter(TimeSpan.FromMilliseconds(5)); + cancellationSource.CancelAfter(System.TimeSpan.FromMilliseconds(5)); try { await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data, cancellationToken); @@ -1045,7 +1045,7 @@ public async Task Test_Async_WriteLargeByteArrayWithCancellationWithMemory() var db = 2; randomEngine.NextBytes(data.Span); - cancellationSource.CancelAfter(TimeSpan.FromMilliseconds(5)); + cancellationSource.CancelAfter(System.TimeSpan.FromMilliseconds(5)); try { await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data, cancellationToken); diff --git a/S7.Net/Enums.cs b/S7.Net/Enums.cs index fc412d3e..080dce65 100644 --- a/S7.Net/Enums.cs +++ b/S7.Net/Enums.cs @@ -206,6 +206,11 @@ public enum VarType /// /// DateTimeLong variable type /// - DateTimeLong + DateTimeLong, + + /// + /// S7 TIME variable type - serialized as S7 DInt and deserialized as C# TimeSpan + /// + Time } } diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs index c0fbd78f..69fc890a 100644 --- a/S7.Net/PLCHelpers.cs +++ b/S7.Net/PLCHelpers.cs @@ -167,6 +167,15 @@ private static void BuildReadDataRequestPackage(System.IO.MemoryStream stream, D { return DateTimeLong.ToArray(bytes); } + case VarType.Time: + if (varCount == 1) + { + return TimeSpan.FromByteArray(bytes); + } + else + { + return TimeSpan.ToArray(bytes); + } default: return null; } @@ -200,6 +209,7 @@ internal static int VarTypeToByteLength(VarType varType, int varCount = 1) case VarType.DWord: case VarType.DInt: case VarType.Real: + case VarType.Time: return varCount * 4; case VarType.LReal: case VarType.DateTime: diff --git a/S7.Net/Types/TimeSpan.cs b/S7.Net/Types/TimeSpan.cs new file mode 100644 index 00000000..37515d8a --- /dev/null +++ b/S7.Net/Types/TimeSpan.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; + +namespace S7.Net.Types +{ + public static class TimeSpan + { + /// + /// The minimum value supported by the specification. + /// + public static readonly System.TimeSpan SpecMinimumTimeSpan = System.TimeSpan.FromMilliseconds(int.MinValue); + + /// + /// The maximum value supported by the specification. + /// + public static readonly System.TimeSpan SpecMaximumTimeSpan = System.TimeSpan.FromMilliseconds(int.MaxValue); + + /// + /// Parses a value from bytes. + /// + /// Input bytes read from PLC. + /// A object representing the value read from PLC. + /// Thrown when the length of + /// is not 4 or any value in + /// is outside the valid range of values. + public static System.TimeSpan FromByteArray(byte[] bytes) + { + var milliseconds = DInt.FromByteArray(bytes); + return System.TimeSpan.FromMilliseconds(milliseconds); + } + + /// + /// Parses an array of values from bytes. + /// + /// Input bytes read from PLC. + /// An array of objects representing the values read from PLC. + /// Thrown when the length of + /// is not a multiple of 4 or any value in + /// is outside the valid range of values. + public static System.TimeSpan[] ToArray(byte[] bytes) + { + const int singleTimeSpanLength = 4; + + if (bytes.Length % singleTimeSpanLength != 0) + throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length, + $"Parsing an array of {nameof(System.TimeSpan)} requires a multiple of {singleTimeSpanLength} bytes of input data, input data is '{bytes.Length}' long."); + + var cnt = bytes.Length / singleTimeSpanLength; + var result = new System.TimeSpan[bytes.Length / singleTimeSpanLength]; + + var milliseconds = DInt.ToArray(bytes); + for (var i = 0; i < milliseconds.Length; i++) + result[i] = System.TimeSpan.FromMilliseconds(milliseconds[i]); + + return result; + } + + /// + /// Converts a value to a byte array. + /// + /// The DateTime value to convert. + /// A byte array containing the S7 date time representation of . + /// Thrown when the value of + /// is before + /// or after . + public static byte[] ToByteArray(System.TimeSpan timeSpan) + { + if (timeSpan < SpecMinimumTimeSpan) + throw new ArgumentOutOfRangeException(nameof(timeSpan), timeSpan, + $"Time span '{timeSpan}' is before the minimum '{SpecMinimumTimeSpan}' supported in S7 time representation."); + + if (timeSpan > SpecMaximumTimeSpan) + throw new ArgumentOutOfRangeException(nameof(timeSpan), timeSpan, + $"Time span '{timeSpan}' is after the maximum '{SpecMaximumTimeSpan}' supported in S7 time representation."); + + return DInt.ToByteArray(Convert.ToInt32(timeSpan.TotalMilliseconds)); + } + + /// + /// Converts an array of values to a byte array. + /// + /// The DateTime values to convert. + /// A byte array containing the S7 date time representations of . + /// Thrown when any value of + /// is before + /// or after . + public static byte[] ToByteArray(System.TimeSpan[] timeSpans) + { + var bytes = new List(timeSpans.Length * 4); + foreach (var timeSpan in timeSpans) bytes.AddRange(ToByteArray(timeSpan)); + + return bytes.ToArray(); + } + } +} \ No newline at end of file From 05ccb05f3a2ac81fa81b04acc54faeb5c35be605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20P=C4=85gowski?= Date: Wed, 19 Jul 2023 20:57:14 +0200 Subject: [PATCH 2/4] Added TimeSpan tests --- S7.Net.UnitTest/TypeTests/TimeSpanTests.cs | 82 ++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 S7.Net.UnitTest/TypeTests/TimeSpanTests.cs diff --git a/S7.Net.UnitTest/TypeTests/TimeSpanTests.cs b/S7.Net.UnitTest/TypeTests/TimeSpanTests.cs new file mode 100644 index 00000000..5f129fb4 --- /dev/null +++ b/S7.Net.UnitTest/TypeTests/TimeSpanTests.cs @@ -0,0 +1,82 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace S7.Net.UnitTest.TypeTests +{ + public static class TimeSpanTests + { + private static readonly TimeSpan SampleTimeSpan = new TimeSpan(12, 0, 59, 37, 856); + + private static readonly byte[] SampleByteArray = { 0x3E, 0x02, 0xE8, 0x00 }; + + private static readonly byte[] SpecMinByteArray = { 0x80, 0x00, 0x00, 0x00 }; + + private static readonly byte[] SpecMaxByteArray = { 0x7F, 0xFF, 0xFF, 0xFF }; + + [TestClass] + public class FromByteArray + { + [TestMethod] + public void Sample() + { + AssertFromByteArrayEquals(SampleTimeSpan, SampleByteArray); + } + + [TestMethod] + public void SpecMinimum() + { + AssertFromByteArrayEquals(Types.TimeSpan.SpecMinimumTimeSpan, SpecMinByteArray); + } + + [TestMethod] + public void SpecMaximum() + { + AssertFromByteArrayEquals(Types.TimeSpan.SpecMaximumTimeSpan, SpecMaxByteArray); + } + + private static void AssertFromByteArrayEquals(TimeSpan expected, params byte[] bytes) + { + Assert.AreEqual(expected, Types.TimeSpan.FromByteArray(bytes)); + } + } + + [TestClass] + public class ToByteArray + { + [TestMethod] + public void Sample() + { + AssertToByteArrayEquals(SampleTimeSpan, SampleByteArray); + } + + [TestMethod] + public void SpecMinimum() + { + AssertToByteArrayEquals(Types.TimeSpan.SpecMinimumTimeSpan, SpecMinByteArray); + } + + [TestMethod] + public void SpecMaximum() + { + AssertToByteArrayEquals(Types.TimeSpan.SpecMaximumTimeSpan, SpecMaxByteArray); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnTimeBeforeSpecMinimum() + { + Types.TimeSpan.ToByteArray(TimeSpan.FromDays(-25)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnTimeAfterSpecMaximum() + { + Types.TimeSpan.ToByteArray(new TimeSpan(30, 15, 15, 15, 15)); + } + + private static void AssertToByteArrayEquals(TimeSpan value, params byte[] expected) + { + CollectionAssert.AreEqual(expected, Types.TimeSpan.ToByteArray(value)); + } + } + } +} From ee06bec0fbeee29aa34d649f52ad3b2cf72935a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20P=C4=85gowski?= Date: Wed, 19 Jul 2023 21:03:45 +0200 Subject: [PATCH 3/4] Fix the documentation --- S7.Net/Types/TimeSpan.cs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/S7.Net/Types/TimeSpan.cs b/S7.Net/Types/TimeSpan.cs index 37515d8a..20f05d87 100644 --- a/S7.Net/Types/TimeSpan.cs +++ b/S7.Net/Types/TimeSpan.cs @@ -3,6 +3,9 @@ namespace S7.Net.Types { + /// + /// Contains the methods to convert between and S7 representation of TIME values. + /// public static class TimeSpan { /// @@ -45,7 +48,6 @@ public static System.TimeSpan[] ToArray(byte[] bytes) throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length, $"Parsing an array of {nameof(System.TimeSpan)} requires a multiple of {singleTimeSpanLength} bytes of input data, input data is '{bytes.Length}' long."); - var cnt = bytes.Length / singleTimeSpanLength; var result = new System.TimeSpan[bytes.Length / singleTimeSpanLength]; var milliseconds = DInt.ToArray(bytes); @@ -56,13 +58,13 @@ public static System.TimeSpan[] ToArray(byte[] bytes) } /// - /// Converts a value to a byte array. + /// Converts a value to a byte array. /// - /// The DateTime value to convert. + /// The TimeSpan value to convert. /// A byte array containing the S7 date time representation of . /// Thrown when the value of - /// is before - /// or after . + /// is before + /// or after . public static byte[] ToByteArray(System.TimeSpan timeSpan) { if (timeSpan < SpecMinimumTimeSpan) @@ -77,13 +79,13 @@ public static byte[] ToByteArray(System.TimeSpan timeSpan) } /// - /// Converts an array of values to a byte array. + /// Converts an array of values to a byte array. /// - /// The DateTime values to convert. - /// A byte array containing the S7 date time representations of . + /// The TimeSpan values to convert. + /// A byte array containing the S7 date time representations of . /// Thrown when any value of - /// is before - /// or after . + /// is before + /// or after . public static byte[] ToByteArray(System.TimeSpan[] timeSpans) { var bytes = new List(timeSpans.Length * 4); From 49e4d3369acf010b5f0a4a90dcc5cd89fba22cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20P=C4=85gowski?= Date: Wed, 19 Jul 2023 21:12:54 +0200 Subject: [PATCH 4/4] Add TimeSpan serialization to Struct --- S7.Net/Types/Struct.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/S7.Net/Types/Struct.cs b/S7.Net/Types/Struct.cs index 1e955086..8fe67fa8 100644 --- a/S7.Net/Types/Struct.cs +++ b/S7.Net/Types/Struct.cs @@ -45,6 +45,7 @@ public static int GetStructSize(Type structType) break; case "Int32": case "UInt32": + case "TimeSpan": numBytes = Math.Ceiling(numBytes); if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) numBytes++; @@ -215,6 +216,21 @@ public static int GetStructSize(Type structType) numBytes += sData.Length; break; + case "TimeSpan": + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + + // get the value + info.SetValue(structValue, TimeSpan.FromByteArray(new[] + { + bytes[(int)numBytes + 0], + bytes[(int)numBytes + 1], + bytes[(int)numBytes + 2], + bytes[(int)numBytes + 3] + })); + numBytes += 4; + break; default: var buffer = new byte[GetStructSize(info.FieldType)]; if (buffer.Length == 0) @@ -303,6 +319,9 @@ public static byte[] ToBytes(object structValue) _ => throw new ArgumentException("Please use a valid string type for the S7StringAttribute") }; break; + case "TimeSpan": + bytes2 = TimeSpan.ToByteArray((System.TimeSpan)info.GetValue(structValue)); + break; } if (bytes2 != null) {