diff --git a/S7.Net.UnitTest/CommunicationTests/Clock.cs b/S7.Net.UnitTest/CommunicationTests/Clock.cs
new file mode 100644
index 0000000..7601aa8
--- /dev/null
+++ b/S7.Net.UnitTest/CommunicationTests/Clock.cs
@@ -0,0 +1,259 @@
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using S7.Net.Protocol;
+
+namespace S7.Net.UnitTest.CommunicationTests;
+
+[TestClass]
+public class Clock
+{
+ [TestMethod, Timeout(1000)]
+ public async Task Read_Clock_Value()
+ {
+ var cs = new CommunicationSequence
+ {
+ ConnectionOpenTemplates.ConnectionRequestConfirm,
+ ConnectionOpenTemplates.CommunicationSetup,
+ {
+ """
+ // TPKT
+ 03 00 00 1d
+
+ // COTP
+ 02 f0 80
+
+ // S7 read clock
+ // UserData header
+ 32 07 00 00 PDU1 PDU2
+ // Parameter length
+ 00 08
+ // Data length
+ 00 04
+
+ // Parameter
+ // Head
+ 00 01 12
+ // Length
+ 04
+ // Method (Request/Response): Req
+ 11
+ // Type request (4...) Function group timers (...7)
+ 47
+ // Subfunction: read clock
+ 01
+ // Sequence number
+ 00
+
+ // Data
+ // Return code
+ 0a
+ // Transport size
+ 00
+ // Payload length
+ 00 00
+ """,
+ """
+ // TPKT
+ 03 00 00 2b
+
+ // COTP
+ 02 f0 80
+
+ // S7 read clock response
+ // UserData header
+ 32 07 00 00 PDU1 PDU2
+ // Parameter length
+ 00 0c
+ // Data length
+ 00 0e
+
+ // Parameter
+ // Head
+ 00 01 12
+ // Length
+ 08
+ // Method (Request/Response): Res
+ 12
+ // Type response (8...) Function group timers (...7)
+ 87
+ // Subfunction: read clock
+ 01
+ // Sequence number
+ 01
+ // Data unit reference
+ 00
+ // Last data unit? Yes
+ 00
+ // Error code
+ 00 00
+
+ // Data
+ // Error code
+ ff
+ // Transport size: OCTET STRING
+ 09
+ // Length
+ 00 0a
+
+ // Timestamp
+ // Reserved
+ 00
+ // Year 1
+ 19
+ // Year 2
+ 14
+ // Month
+ 08
+ // Day
+ 20
+ // Hour
+ 11
+ // Minute
+ 59
+ // Seconds
+ 43
+ // Milliseconds: 912..., Day of week: ...4
+ 91 24
+ """
+ }
+ };
+
+ static async Task Client(int port)
+ {
+ var conn = new Plc(IPAddress.Loopback.ToString(), port, new TsapPair(new Tsap(1, 2), new Tsap(3, 4)));
+ await conn.OpenAsync();
+ var time = await conn.ReadClockAsync();
+
+ Assert.AreEqual(new DateTime(2014, 8, 20, 11, 59, 43, 912), time);
+ conn.Close();
+ }
+
+ await Task.WhenAll(cs.Serve(out var port), Client(port));
+ }
+
+ [TestMethod, Timeout(1000)]
+ public async Task Write_Clock_Value()
+ {
+ var cs = new CommunicationSequence
+ {
+ ConnectionOpenTemplates.ConnectionRequestConfirm,
+ ConnectionOpenTemplates.CommunicationSetup,
+ {
+ """
+ // TPKT
+ 03 00 00 27
+
+ // COTP
+ 02 f0 80
+
+ // S7 read clock
+ // UserData header
+ 32 07 00 00 PDU1 PDU2
+ // Parameter length
+ 00 08
+ // Data length
+ 00 0e
+
+ // Parameter
+ // Head
+ 00 01 12
+ // Length
+ 04
+ // Method (Request/Response): Req
+ 11
+ // Type request (4...) Function group timers (...7)
+ 47
+ // Subfunction: write clock
+ 02
+ // Sequence number
+ 00
+
+ // Data
+ // Return code
+ ff
+ // Transport size
+ 09
+ // Payload length
+ 00 0a
+
+ // Payload
+ // Timestamp
+ // Reserved
+ 00
+ // Year 1
+ 19
+ // Year 2
+ 14
+ // Month
+ 08
+ // Day
+ 20
+ // Hour
+ 11
+ // Minute
+ 59
+ // Seconds
+ 43
+ // Milliseconds: 912..., Day of week: ...4
+ 91 24
+ """,
+ """
+ // TPKT
+ 03 00 00 21
+
+ // COTP
+ 02 f0 80
+
+ // S7 read clock response
+ // UserData header
+ 32 07 00 00 PDU1 PDU2
+ // Parameter length
+ 00 0c
+ // Data length
+ 00 04
+
+ // Parameter
+ // Head
+ 00 01 12
+ // Length
+ 08
+ // Method (Request/Response): Res
+ 12
+ // Type response (8...) Function group timers (...7)
+ 87
+ // Subfunction: write clock
+ 02
+ // Sequence number
+ 01
+ // Data unit reference
+ 00
+ // Last data unit? Yes
+ 00
+ // Error code
+ 00 00
+
+ // Data
+ // Error code
+ 0a
+ // Transport size: NONE
+ 00
+ // Length
+ 00 00
+ """
+ }
+ };
+
+ static async Task Client(int port)
+ {
+ var conn = new Plc(IPAddress.Loopback.ToString(), port, new TsapPair(new Tsap(1, 2), new Tsap(3, 4)));
+ await conn.OpenAsync();
+ await conn.WriteClockAsync(new DateTime(2014, 08, 20, 11, 59, 43, 912));
+
+ conn.Close();
+ }
+
+ await Task.WhenAll(cs.Serve(out var port), Client(port));
+ }
+}
\ No newline at end of file
diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs
index fa01672..fc6bb14 100644
--- a/S7.Net/PLCHelpers.cs
+++ b/S7.Net/PLCHelpers.cs
@@ -56,28 +56,35 @@ namespace S7.Net
WriteS7Header(stream, s7MessageTypeUserData, parameterLength, dataLength);
}
- private static void WriteSzlReadRequest(System.IO.MemoryStream stream, ushort szlId, ushort szlIndex)
+ private static void WriteUserDataRequest(System.IO.MemoryStream stream, byte functionGroup, byte subFunction, int dataLength)
{
- WriteUserDataHeader(stream, 8, 8);
+ WriteUserDataHeader(stream, 8, dataLength);
// Parameter
- const byte szlMethodRequest = 0x11;
- const byte szlTypeRequest = 0b100;
- const byte szlFunctionGroupCpuFunctions = 0b100;
- const byte subFunctionReadSzl = 0x01;
+ const byte userDataMethodRequest = 0x11;
+ const byte userDataTypeRequest = 0x4;
// Parameter head
stream.Write(new byte[] { 0x00, 0x01, 0x12 });
// Parameter length
stream.WriteByte(0x04);
// Method
- stream.WriteByte(szlMethodRequest);
+ stream.WriteByte(userDataMethodRequest);
// Type / function group
- stream.WriteByte(szlTypeRequest << 4 | szlFunctionGroupCpuFunctions);
+ stream.WriteByte((byte)(userDataTypeRequest << 4 | (functionGroup & 0x0f)));
// Subfunction
- stream.WriteByte(subFunctionReadSzl);
+ stream.WriteByte(subFunction);
// Sequence number
stream.WriteByte(0);
+ }
+
+ private static void WriteSzlReadRequest(System.IO.MemoryStream stream, ushort szlId, ushort szlIndex)
+ {
+ // Parameter
+ const byte szlFunctionGroupCpuFunctions = 0b100;
+ const byte subFunctionReadSzl = 0x01;
+
+ WriteUserDataRequest(stream, szlFunctionGroupCpuFunctions, subFunctionReadSzl, 8);
// Data
const byte success = 0xff;
@@ -343,7 +350,7 @@ namespace S7.Net
private static byte[] BuildSzlReadRequestPackage(ushort szlId, ushort szlIndex)
{
var stream = new System.IO.MemoryStream();
-
+
WriteSzlReadRequest(stream, szlId, szlIndex);
stream.SetLength(stream.Position);
diff --git a/S7.Net/Plc.Clock.cs b/S7.Net/Plc.Clock.cs
new file mode 100644
index 0000000..0aba639
--- /dev/null
+++ b/S7.Net/Plc.Clock.cs
@@ -0,0 +1,92 @@
+using System;
+using System.IO;
+using System.Linq;
+using S7.Net.Helper;
+using S7.Net.Types;
+using DateTime = System.DateTime;
+
+namespace S7.Net;
+
+partial class Plc
+{
+ private const byte SzlFunctionGroupTimers = 0x07;
+ private const byte SzlSubFunctionReadClock = 0x01;
+ private const byte SzlSubFunctionWriteClock = 0x02;
+ private const byte TransportSizeOctetString = 0x09;
+ private const int PduErrOffset = 20;
+ private const int UserDataResultOffset = PduErrOffset + 2;
+
+ ///
+ /// The length in bytes of DateTime stored in the PLC.
+ ///
+ private const int DateTimeLength = 10;
+
+ private static byte[] BuildClockReadRequest()
+ {
+ var stream = new MemoryStream();
+
+ WriteUserDataRequest(stream, SzlFunctionGroupTimers, SzlSubFunctionReadClock, 4);
+ stream.Write(new byte[] { 0x0a, 0x00, 0x00, 0x00 });
+
+ stream.SetLength(stream.Position);
+ return stream.ToArray();
+ }
+
+ private static DateTime ParseClockReadResponse(byte[] message)
+ {
+ const int udLenOffset = UserDataResultOffset + 2;
+ const int udValueOffset = udLenOffset + 2;
+ const int dateTimeSkip = 2;
+
+ AssertPduResult(message);
+ AssertUserDataResult(message, 0xff);
+
+ var len = Word.FromByteArray(message.Skip(udLenOffset).Take(2).ToArray());
+ if (len != DateTimeLength)
+ {
+ throw new Exception($"Unexpected response length {len}, expected {DateTimeLength}.");
+ }
+
+ // Skip first 2 bytes from date time value because DateTime.FromByteArray doesn't parse them.
+ return Types.DateTime.FromByteArray(message.Skip(udValueOffset + dateTimeSkip)
+ .Take(DateTimeLength - dateTimeSkip).ToArray());
+ }
+
+ private static byte[] BuildClockWriteRequest(DateTime value)
+ {
+ var stream = new MemoryStream();
+
+ WriteUserDataRequest(stream, SzlFunctionGroupTimers, SzlSubFunctionWriteClock, 14);
+ stream.Write(new byte[] { 0xff, TransportSizeOctetString, 0x00, DateTimeLength });
+ // Start of DateTime value, DateTime.ToByteArray only serializes the final 8 bytes
+ stream.Write(new byte[] { 0x00, 0x19 });
+ stream.Write(Types.DateTime.ToByteArray(value));
+
+ stream.SetLength(stream.Position);
+ return stream.ToArray();
+ }
+
+ private static void ParseClockWriteResponse(byte[] message)
+ {
+ AssertPduResult(message);
+ AssertUserDataResult(message, 0x0a);
+ }
+
+ private static void AssertPduResult(byte[] message)
+ {
+ var pduErr = Word.FromByteArray(message.Skip(PduErrOffset).Take(2).ToArray());
+ if (pduErr != 0)
+ {
+ throw new Exception($"Response from PLC indicates error 0x{pduErr:X4}.");
+ }
+ }
+
+ private static void AssertUserDataResult(byte[] message, byte expected)
+ {
+ var dtResult = message[UserDataResultOffset];
+ if (dtResult != expected)
+ {
+ throw new Exception($"Response from PLC was 0x{dtResult:X2}, expected 0x{expected:X2}.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/S7.Net/PlcAsynchronous.cs b/S7.Net/PlcAsynchronous.cs
index eb49e5d..8b828f3 100644
--- a/S7.Net/PlcAsynchronous.cs
+++ b/S7.Net/PlcAsynchronous.cs
@@ -312,6 +312,35 @@ namespace S7.Net
return dataItems;
}
+ ///
+ /// Read the PLC clock value.
+ ///
+ /// The token to monitor for cancellation requests. The default value is None.
+ /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.
+ /// A task that represents the asynchronous operation, with it's result set to the current PLC time on completion.
+ public async Task ReadClockAsync(CancellationToken cancellationToken = default)
+ {
+ var request = BuildClockReadRequest();
+ var response = await RequestTsduAsync(request, cancellationToken);
+
+ return ParseClockReadResponse(response);
+ }
+
+ ///
+ /// Write the PLC clock value.
+ ///
+ /// The date and time to set the PLC clock to
+ /// The token to monitor for cancellation requests. The default value is None.
+ /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.
+ /// A task that represents the asynchronous operation.
+ public async Task WriteClockAsync(System.DateTime value, CancellationToken cancellationToken = default)
+ {
+ var request = BuildClockWriteRequest(value);
+ var response = await RequestTsduAsync(request, cancellationToken);
+
+ ParseClockWriteResponse(response);
+ }
+
///
/// Read the current status from the PLC. A value of 0x08 indicates the PLC is in run status, regardless of the PLC type.
///
diff --git a/S7.Net/PlcSynchronous.cs b/S7.Net/PlcSynchronous.cs
index 1b3af97..2e28131 100644
--- a/S7.Net/PlcSynchronous.cs
+++ b/S7.Net/PlcSynchronous.cs
@@ -492,6 +492,30 @@ namespace S7.Net
}
}
+ ///
+ /// Read the PLC clock value.
+ ///
+ /// The current PLC time.
+ public System.DateTime ReadClock()
+ {
+ var request = BuildClockReadRequest();
+ var response = RequestTsdu(request);
+
+ return ParseClockReadResponse(response);
+ }
+
+ ///
+ /// Write the PLC clock value.
+ ///
+ /// The date and time to set the PLC clock to.
+ public void WriteClock(System.DateTime value)
+ {
+ var request = BuildClockWriteRequest(value);
+ var response = RequestTsdu(request);
+
+ ParseClockWriteResponse(response);
+ }
+
///
/// Read the current status from the PLC. A value of 0x08 indicates the PLC is in run status, regardless of the PLC type.
///