From 5d59c8284dc1e759cc0ee7f42a1f12f683cfcebc Mon Sep 17 00:00:00 2001 From: Serge Camille Date: Sun, 16 Aug 2020 22:31:26 +0200 Subject: [PATCH] Add DTL type Add new class Types.Dtl by taking the DateTime type and adjusting things. Also add unit test with binary data calculated by hand. (Need to verify with actual S7 data) --- S7.Net.UnitTest/S7.Net.UnitTest.csproj | 1 + S7.Net.UnitTest/TypeTests/DtlTests.cs | 171 +++++++++++++++++++++++++ S7.Net/Types/Dtl.cs | 158 +++++++++++++++++++++++ 3 files changed, 330 insertions(+) create mode 100644 S7.Net.UnitTest/TypeTests/DtlTests.cs create mode 100644 S7.Net/Types/Dtl.cs diff --git a/S7.Net.UnitTest/S7.Net.UnitTest.csproj b/S7.Net.UnitTest/S7.Net.UnitTest.csproj index 0a14d88..a87d0e2 100644 --- a/S7.Net.UnitTest/S7.Net.UnitTest.csproj +++ b/S7.Net.UnitTest/S7.Net.UnitTest.csproj @@ -81,6 +81,7 @@ + diff --git a/S7.Net.UnitTest/TypeTests/DtlTests.cs b/S7.Net.UnitTest/TypeTests/DtlTests.cs new file mode 100644 index 0000000..23b6217 --- /dev/null +++ b/S7.Net.UnitTest/TypeTests/DtlTests.cs @@ -0,0 +1,171 @@ +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace S7.Net.UnitTest.TypeTests +{ + public static class DtlTests + { + private static readonly DateTime SampleDateTime = new DateTime(1993, 12, 25, 8, 12, 34, 567); + + private static readonly byte[] SampleByteArray = {0x07, 0xC9, 0x0C, 0x19, 0x07, 0x08, 0x0C, 0x22, 0x21, 0xCB, 0xBB, 0xC0 }; + + private static readonly byte[] SpecMinByteArray = + { + 0x07, 0xB2, 0x01, 0x01, (byte) (int) (Types.Dtl.SpecMinimumDateTime.DayOfWeek + 1), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + private static readonly byte[] SpecMaxByteArray = + { + 0x08, 0xD6, 0x04, 0x0B, (byte) (int) (Types.Dtl.SpecMaximumDateTime.DayOfWeek + 1), 0x17, 0x2F, 0x10, 0x32, 0xE7, 0x01, 0x80 + }; + + [TestClass] + public class FromByteArray + { + [TestMethod] + public void Sample() + { + AssertFromByteArrayEquals(SampleDateTime, SampleByteArray); + } + + [TestMethod] + public void SpecMinimum() + { + AssertFromByteArrayEquals(Types.Dtl.SpecMinimumDateTime, SpecMinByteArray); + } + + [TestMethod] + public void SpecMaximum() + { + AssertFromByteArrayEquals(Types.Dtl.SpecMaximumDateTime, SpecMaxByteArray); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnLessThan12Bytes() + { + Types.Dtl.FromByteArray(new byte[11]); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnMoreTHan12Bytes() + { + Types.Dtl.FromByteArray(new byte[13]); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnInvalidYear() + { + Types.Dtl.FromByteArray(MutateSample(0, 0xa0)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnZeroMonth() + { + Types.Dtl.FromByteArray(MutateSample(2, 0x00)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnTooLargeMonth() + { + Types.Dtl.FromByteArray(MutateSample(2, 0x13)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnZeroDay() + { + Types.Dtl.FromByteArray(MutateSample(3, 0x00)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnTooLargeDay() + { + Types.Dtl.FromByteArray(MutateSample(3, 0x32)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnInvalidHour() + { + Types.Dtl.FromByteArray(MutateSample(5, 0x24)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnInvalidMinute() + { + Types.Dtl.FromByteArray(MutateSample(6, 0x60)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnInvalidSecond() + { + Types.Dtl.FromByteArray(MutateSample(7, 0x60)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnInvalidNanosecondsFirstDigit() + { + Types.Dtl.FromByteArray(MutateSample(8, 0x3B)); + } + + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnZeroDayOfWeek() + { + Types.Dtl.FromByteArray(MutateSample(4, 0)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnTooLargeDayOfWeek() + { + Types.Dtl.FromByteArray(MutateSample(4, 8)); + } + + private static void AssertFromByteArrayEquals(DateTime expected, params byte[] bytes) + { + Assert.AreEqual(expected, Types.Dtl.FromByteArray(bytes)); + } + + private static byte[] MutateSample(int index, byte value) => + SampleByteArray.Select((b, i) => i == index ? value : b).ToArray(); + } + + [TestClass] + public class ToByteArray + { + [TestMethod] + public void Sample() + { + AssertToByteArrayEquals(SampleDateTime, SampleByteArray); + } + + [TestMethod] + public void SpecMinimum() + { + AssertToByteArrayEquals(Types.Dtl.SpecMinimumDateTime, SpecMinByteArray); + } + + [TestMethod] + public void SpecMaximum() + { + AssertToByteArrayEquals(Types.Dtl.SpecMaximumDateTime, SpecMaxByteArray); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnTimeBeforeSpecMinimum() + { + Types.Dtl.ToByteArray(new DateTime(1950, 1, 1)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnTimeAfterSpecMaximum() + { + Types.Dtl.ToByteArray(new DateTime(2790, 1, 1)); + } + + private static void AssertToByteArrayEquals(DateTime value, params byte[] expected) + { + CollectionAssert.AreEqual(expected, Types.Dtl.ToByteArray(value)); + } + } + } +} diff --git a/S7.Net/Types/Dtl.cs b/S7.Net/Types/Dtl.cs new file mode 100644 index 0000000..1c95911 --- /dev/null +++ b/S7.Net/Types/Dtl.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace S7.Net.Types +{ + /// + /// Contains the methods to convert between and S7 representation of DTL values. + /// + public static class Dtl + { + /// + /// The minimum value supported by the specification. + /// + public static readonly System.DateTime SpecMinimumDateTime = new System.DateTime(1970, 1, 1); + + /// + /// The maximum value supported by the specification. + /// + public static readonly System.DateTime SpecMaximumDateTime = new System.DateTime(2262, 4, 11, 23, 47, 16, 854); + + /// + /// 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 12 or any value in + /// is outside the valid range of values. + public static System.DateTime FromByteArray(byte[] bytes) + { + return FromByteArrayImpl(bytes); + } + + /// + /// 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 12 or any value in + /// is outside the valid range of values. + public static System.DateTime[] ToArray(byte[] bytes) + { + if (bytes.Length % 12 != 0) + throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length, + $"Parsing an array of Dtl requires a multiple of 12 bytes of input data, input data is '{bytes.Length}' long."); + + var cnt = bytes.Length / 12; + var result = new System.DateTime[cnt]; + + for (var i = 0; i < cnt; i++) + { + var slice = new byte[12]; + Array.Copy(bytes, i * 12, slice, 0, 12); + result[i] = FromByteArrayImpl(slice); + } + + return result; + } + + private static System.DateTime FromByteArrayImpl(byte[] bytes) + { + if (bytes.Length != 12) + throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length, + $"Parsing a Dtl requires exactly 12 bytes of input data, input data is {bytes.Length} bytes long."); + + + int year = AssertRangeInclusive(Word.FromBytes(bytes[1], bytes[0]), 1970, 2262, "year"); + int month = AssertRangeInclusive(bytes[2], 1, 12, "month"); + int day = AssertRangeInclusive(bytes[3], 1, 31, "day of month"); + var dayOfWeek = AssertRangeInclusive(bytes[4], 1, 7, "day of week"); + int hour = AssertRangeInclusive(bytes[5], 0, 23, "hour"); + int minute = AssertRangeInclusive(bytes[6], 0, 59, "minute"); + int second = AssertRangeInclusive(bytes[7], 0, 59, "second"); ; + + var nanoseconds = AssertRangeInclusive(DWord.FromBytes(bytes[11], bytes[10], bytes[9], bytes[8]), 0, 999999999, "nanoseconds"); + + var time = new System.DateTime(year, month, day, hour, minute, second); + return time.AddTicks(nanoseconds / 100); + } + + /// + /// Converts a value to a byte array. + /// + /// The DateTime value to convert. + /// A byte array containing the S7 DTL representation of . + /// Thrown when the value of + /// is before + /// or after . + public static byte[] ToByteArray(System.DateTime dateTime) + { + if (dateTime < SpecMinimumDateTime) + throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime, + $"Date time '{dateTime}' is before the minimum '{SpecMinimumDateTime}' supported in S7 DTL representation."); + + if (dateTime > SpecMaximumDateTime) + throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime, + $"Date time '{dateTime}' is after the maximum '{SpecMaximumDateTime}' supported in S7 DTL representation."); + + var stream = new MemoryStream(12); + // Convert Year + stream.Write(Word.ToByteArray(Convert.ToUInt16(dateTime.Year)), 0, 2); + + // Convert Month + stream.WriteByte(Convert.ToByte(dateTime.Month)); + + // Convert Day + stream.WriteByte(Convert.ToByte(dateTime.Day)); + + // Convert WeekDay. NET DateTime starts with Sunday = 0, while S7DT has Sunday = 1. + stream.WriteByte(Convert.ToByte(dateTime.DayOfWeek + 1)); + + // Convert Hour + stream.WriteByte(Convert.ToByte(dateTime.Hour)); + + // Convert Minutes + stream.WriteByte(Convert.ToByte(dateTime.Minute)); + + // Convert Seconds + stream.WriteByte(Convert.ToByte(dateTime.Second)); + + // Convert Nanoseconds. Net DateTime has a representation of 1 Tick = 100ns. + // Thus First take the ticks Mod 1 Second (1s = 10'000'000 ticks), and then Convert to nanoseconds. + stream.Write(DWord.ToByteArray(Convert.ToUInt32(dateTime.Ticks % 10000000 * 100)), 0, 4); + + return stream.ToArray(); + } + + /// + /// Converts an array of values to a byte array. + /// + /// The DateTime values to convert. + /// A byte array containing the S7 DTL representations of . + /// Thrown when any value of + /// is before + /// or after . + public static byte[] ToByteArray(System.DateTime[] dateTimes) + { + var bytes = new List(dateTimes.Length * 8); + foreach (var dateTime in dateTimes) bytes.AddRange(ToByteArray(dateTime)); + + return bytes.ToArray(); + } + + private static T AssertRangeInclusive(T input, T min, T max, string field) where T : IComparable + { + if (input.CompareTo(min) < 0) + throw new ArgumentOutOfRangeException(nameof(input), input, + $"Value '{input}' is lower than the minimum '{min}' allowed for {field}."); + if (input.CompareTo(max) > 0) + throw new ArgumentOutOfRangeException(nameof(input), input, + $"Value '{input}' is higher than the maximum '{max}' allowed for {field}."); + + return input; + } + } +} \ No newline at end of file