diff --git a/S7.Net.UnitTest/S7.Net.UnitTest.csproj b/S7.Net.UnitTest/S7.Net.UnitTest.csproj index dabda6d..321f0aa 100644 --- a/S7.Net.UnitTest/S7.Net.UnitTest.csproj +++ b/S7.Net.UnitTest/S7.Net.UnitTest.csproj @@ -79,6 +79,7 @@ + diff --git a/S7.Net.UnitTest/TypeTests/DateTimeTests.cs b/S7.Net.UnitTest/TypeTests/DateTimeTests.cs new file mode 100644 index 0000000..d87e31f --- /dev/null +++ b/S7.Net.UnitTest/TypeTests/DateTimeTests.cs @@ -0,0 +1,176 @@ +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace S7.Net.UnitTest.TypeTests +{ + public static class DateTimeTests + { + private static readonly DateTime SampleDateTime = new DateTime(1993, 12, 25, 8, 12, 34, 567); + + private static readonly byte[] SampleByteArray = {0x93, 0x12, 0x25, 0x08, 0x12, 0x34, 0x56, 7 << 4 | 7}; + + private static readonly byte[] SpecMinByteArray = + { + 0x90, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, (byte) (int) (Types.DateTime.SpecMinimumDateTime.DayOfWeek + 1) + }; + + private static readonly byte[] SpecMaxByteArray = + { + 0x89, 0x12, 0x31, 0x23, 0x59, 0x59, 0x99, (byte) (9 << 4 | (int) (Types.DateTime.SpecMaximumDateTime.DayOfWeek + 1)) + }; + + [TestClass] + public class FromByteArray + { + [TestMethod] + public void Sample() + { + AssertFromByteArrayEquals(SampleDateTime, SampleByteArray); + } + + [TestMethod] + public void SpecMinimum() + { + AssertFromByteArrayEquals(Types.DateTime.SpecMinimumDateTime, SpecMinByteArray); + } + + [TestMethod] + public void SpecMaximum() + { + AssertFromByteArrayEquals(Types.DateTime.SpecMaximumDateTime, SpecMaxByteArray); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnLessThan8Bytes() + { + Types.DateTime.FromByteArray(new byte[7]); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnMoreTHan8Bytes() + { + Types.DateTime.FromByteArray(new byte[9]); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnInvalidYear() + { + Types.DateTime.FromByteArray(MutateSample(0, 0xa0)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnZeroMonth() + { + Types.DateTime.FromByteArray(MutateSample(1, 0x00)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnTooLargeMonth() + { + Types.DateTime.FromByteArray(MutateSample(1, 0x13)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnZeroDay() + { + Types.DateTime.FromByteArray(MutateSample(2, 0x00)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnTooLargeDay() + { + Types.DateTime.FromByteArray(MutateSample(2, 0x32)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnInvalidHour() + { + Types.DateTime.FromByteArray(MutateSample(3, 0x24)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnInvalidMinute() + { + Types.DateTime.FromByteArray(MutateSample(4, 0x60)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnInvalidSecond() + { + Types.DateTime.FromByteArray(MutateSample(5, 0x60)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnInvalidFirstTwoMillisecondDigits() + { + Types.DateTime.FromByteArray(MutateSample(6, 0xa0)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnInvalidThirdMillisecondDigit() + { + Types.DateTime.FromByteArray(MutateSample(7, 10 << 4)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnZeroDayOfWeek() + { + Types.DateTime.FromByteArray(MutateSample(7, 0)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnTooLargeDayOfWeek() + { + Types.DateTime.FromByteArray(MutateSample(7, 8)); + } + + private static void AssertFromByteArrayEquals(DateTime expected, params byte[] bytes) + { + Assert.AreEqual(expected, Types.DateTime.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.DateTime.SpecMinimumDateTime, SpecMinByteArray); + } + + [TestMethod] + public void SpecMaximum() + { + AssertToByteArrayEquals(Types.DateTime.SpecMaximumDateTime, SpecMaxByteArray); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnTimeBeforeSpecMinimum() + { + Types.DateTime.ToByteArray(new DateTime(1970, 1, 1)); + } + + [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void ThrowsOnTimeAfterSpecMaximum() + { + Types.DateTime.ToByteArray(new DateTime(2090, 1, 1)); + } + + private static void AssertToByteArrayEquals(DateTime value, params byte[] expected) + { + CollectionAssert.AreEqual(expected, Types.DateTime.ToByteArray(value)); + } + } + } +} diff --git a/S7.Net/Enums.cs b/S7.Net/Enums.cs index ff33d04..f4b4778 100644 --- a/S7.Net/Enums.cs +++ b/S7.Net/Enums.cs @@ -181,6 +181,11 @@ /// /// Counter variable type /// - Counter + Counter, + + /// + /// DateTIme variable type + /// + DateTime } } diff --git a/S7.Net/PLC.cs b/S7.Net/PLC.cs index 853c4ed..d62e783 100644 --- a/S7.Net/PLC.cs +++ b/S7.Net/PLC.cs @@ -23,6 +23,11 @@ namespace S7.Net /// public string IP { get; private set; } + /// + /// PORT Number of the PLC, default is 102 + /// + public int Port { get; private set; } + /// /// CPU type of the PLC /// @@ -107,7 +112,34 @@ namespace S7.Net catch { return false; } } } - + + /// + /// Creates a PLC object with all the parameters needed for connections. + /// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0. + /// You need slot > 0 if you are connecting to external ethernet card (CP). + /// For S7-300 and S7-400 the default is rack = 0 and slot = 2. + /// + /// CpuType of the PLC (select from the enum) + /// Ip address of the PLC + /// Port address of the PLC, default 102 + /// rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal + /// slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500. + /// If you use an external ethernet card, this must be set accordingly. + public Plc(CpuType cpu, string ip, int port, Int16 rack, Int16 slot) + { + if (!Enum.IsDefined(typeof(CpuType), cpu)) + throw new ArgumentException($"The value of argument '{nameof(cpu)}' ({cpu}) is invalid for Enum type '{typeof(CpuType).Name}'.", nameof(cpu)); + + if (string.IsNullOrEmpty(ip)) + throw new ArgumentException("IP address must valid.", nameof(ip)); + + CPU = cpu; + IP = ip; + Port = port; + Rack = rack; + Slot = slot; + MaxPDUSize = 240; + } /// /// Creates a PLC object with all the parameters needed for connections. /// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0. @@ -129,6 +161,7 @@ namespace S7.Net CPU = cpu; IP = ip; + Port = 102; Rack = rack; Slot = slot; MaxPDUSize = 240; diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs index d4e5f18..803d349 100644 --- a/S7.Net/PLCHelpers.cs +++ b/S7.Net/PLCHelpers.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; +using DateTime = S7.Net.Types.DateTime; namespace S7.Net { @@ -146,6 +147,15 @@ namespace S7.Net { return Bit.ToBitArray(bytes); } + case VarType.DateTime: + if (varCount == 1) + { + return DateTime.FromByteArray(bytes); + } + else + { + return DateTime.ToArray(bytes); + } default: return null; } @@ -178,6 +188,8 @@ namespace S7.Net case VarType.DInt: case VarType.Real: return varCount * 4; + case VarType.DateTime: + return varCount * 8; default: return 0; } diff --git a/S7.Net/PlcAsynchronous.cs b/S7.Net/PlcAsynchronous.cs index 291a938..eb44f87 100644 --- a/S7.Net/PlcAsynchronous.cs +++ b/S7.Net/PlcAsynchronous.cs @@ -45,7 +45,7 @@ namespace S7.Net { tcpClient = new TcpClient(); ConfigureConnection(); - await tcpClient.ConnectAsync(IP, 102); + await tcpClient.ConnectAsync(IP, Port); stream = tcpClient.GetStream(); } diff --git a/S7.Net/PlcSynchronous.cs b/S7.Net/PlcSynchronous.cs index a027249..4d432d3 100644 --- a/S7.Net/PlcSynchronous.cs +++ b/S7.Net/PlcSynchronous.cs @@ -51,7 +51,7 @@ namespace S7.Net { tcpClient = new TcpClient(); ConfigureConnection(); - tcpClient.Connect(IP, 102); + tcpClient.Connect(IP, Port); stream = tcpClient.GetStream(); } catch (SocketException sex) diff --git a/S7.Net/Protocol/Serialization.cs b/S7.Net/Protocol/Serialization.cs index 40cb629..613c9f4 100644 --- a/S7.Net/Protocol/Serialization.cs +++ b/S7.Net/Protocol/Serialization.cs @@ -41,6 +41,8 @@ namespace S7.Net.Protocol return Types.Double.ToByteArray((double)value); case "Single": return Types.Single.ToByteArray((float)value); + case "DateTime": + return Types.DateTime.ToByteArray((System.DateTime) value); case "Byte[]": return (byte[])value; case "Int16[]": @@ -60,6 +62,8 @@ namespace S7.Net.Protocol // if the consumer does not pay attention to string length. var stringVal = (string) value; return Types.String.ToByteArray(stringVal, stringVal.Length); + case "DateTime[]": + return Types.DateTime.ToByteArray((System.DateTime[]) value); default: throw new InvalidVariableTypeException(); } diff --git a/S7.Net/Types/DateTime.cs b/S7.Net/Types/DateTime.cs new file mode 100644 index 0000000..9cafa67 --- /dev/null +++ b/S7.Net/Types/DateTime.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; + +namespace S7.Net.Types +{ + /// + /// Contains the methods to convert between and S7 representation of datetime values. + /// + public static class DateTime + { + /// + /// The minimum value supported by the specification. + /// + public static readonly System.DateTime SpecMinimumDateTime = new System.DateTime(1990, 1, 1); + + /// + /// The maximum value supported by the specification. + /// + public static readonly System.DateTime SpecMaximumDateTime = new System.DateTime(2089, 12, 31, 23, 59, 59, 999); + + /// + /// 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 8 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 8 or any value in + /// is outside the valid range of values. + public static System.DateTime[] ToArray(byte[] bytes) + { + if (bytes.Length % 8 != 0) + throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length, + $"Parsing an array of DateTime requires a multiple of 8 bytes of input data, input data is '{bytes.Length}' long."); + + var cnt = bytes.Length / 8; + var result = new System.DateTime[bytes.Length / 8]; + + for (var i = 0; i < cnt; i++) + result[i] = FromByteArrayImpl(new ArraySegment(bytes, i * 8, 8)); + + return result; + } + + private static System.DateTime FromByteArrayImpl(IList bytes) + { + if (bytes.Count != 8) + throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Count, + $"Parsing a DateTime requires exactly 8 bytes of input data, input data is {bytes.Count} bytes long."); + + int DecodeBcd(byte input) => 10 * (input >> 4) + (input & 0b00001111); + + int ByteToYear(byte bcdYear) + { + var input = DecodeBcd(bcdYear); + if (input < 90) return input + 2000; + if (input < 100) return input + 1900; + + throw new ArgumentOutOfRangeException(nameof(bcdYear), bcdYear, + $"Value '{input}' is higher than the maximum '99' of S7 date and time representation."); + } + + int AssertRangeInclusive(int input, byte min, byte max, string field) + { + if (input < min) + throw new ArgumentOutOfRangeException(nameof(input), input, + $"Value '{input}' is lower than the minimum '{min}' allowed for {field}."); + if (input > max) + throw new ArgumentOutOfRangeException(nameof(input), input, + $"Value '{input}' is higher than the maximum '{max}' allowed for {field}."); + + return input; + } + + var year = ByteToYear(bytes[0]); + var month = AssertRangeInclusive(DecodeBcd(bytes[1]), 1, 12, "month"); + var day = AssertRangeInclusive(DecodeBcd(bytes[2]), 1, 31, "day of month"); + var hour = AssertRangeInclusive(DecodeBcd(bytes[3]), 0, 23, "hour"); + var minute = AssertRangeInclusive(DecodeBcd(bytes[4]), 0, 59, "minute"); + var second = AssertRangeInclusive(DecodeBcd(bytes[5]), 0, 59, "second"); + var hsec = AssertRangeInclusive(DecodeBcd(bytes[6]), 0, 99, "first two millisecond digits"); + var msec = AssertRangeInclusive(bytes[7] >> 4, 0, 9, "third millisecond digit"); + var dayOfWeek = AssertRangeInclusive(bytes[7] & 0b00001111, 1, 7, "day of week"); + + return new System.DateTime(year, month, day, hour, minute, second, hsec * 10 + msec); + } + + /// + /// 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.DateTime dateTime) + { + byte EncodeBcd(int value) + { + return (byte) ((value / 10 << 4) | value % 10); + } + + if (dateTime < SpecMinimumDateTime) + throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime, + $"Date time '{dateTime}' is before the minimum '{SpecMinimumDateTime}' supported in S7 date time representation."); + + if (dateTime > SpecMaximumDateTime) + throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime, + $"Date time '{dateTime}' is after the maximum '{SpecMaximumDateTime}' supported in S7 date time representation."); + + byte MapYear(int year) => (byte) (year < 2000 ? year - 1900 : year - 2000); + + int DayOfWeekToInt(DayOfWeek dayOfWeek) => (int) dayOfWeek + 1; + + return new[] + { + EncodeBcd(MapYear(dateTime.Year)), + EncodeBcd(dateTime.Month), + EncodeBcd(dateTime.Day), + EncodeBcd(dateTime.Hour), + EncodeBcd(dateTime.Minute), + EncodeBcd(dateTime.Second), + EncodeBcd(dateTime.Millisecond / 10), + (byte) (dateTime.Millisecond % 10 << 4 | DayOfWeekToInt(dateTime.DayOfWeek)) + }; + } + + /// + /// 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.DateTime[] dateTimes) + { + var bytes = new List(dateTimes.Length * 8); + foreach (var dateTime in dateTimes) bytes.AddRange(ToByteArray(dateTime)); + + return bytes.ToArray(); + } + } +} \ No newline at end of file diff --git a/S7.Net/Types/StringEx.cs b/S7.Net/Types/StringEx.cs index 1aec9ab..81733ca 100644 --- a/S7.Net/Types/StringEx.cs +++ b/S7.Net/Types/StringEx.cs @@ -22,7 +22,17 @@ namespace S7.Net.Types int size = bytes[0]; int length = bytes[1]; - return System.Text.Encoding.ASCII.GetString(bytes, 2, length); + try + { + return Encoding.ASCII.GetString(bytes, 2, length); + } + catch (Exception e) + { + throw new PlcException(ErrorCode.ReadData, + $"Failed to parse {VarType.StringEx} from data. Following fields were read: size: '{size}', actual length: '{length}', total number of bytes (including header): '{bytes.Length}'.", + e); + } + } ///