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