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);
+ }
+
}
///