From 6fc526b8866e1d679d5fdb2b1e87c011d74931a7 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Wed, 23 Aug 2023 22:40:57 +0200 Subject: [PATCH 01/13] Stub PLC ReadClock methods --- S7.Net/PlcAsynchronous.cs | 11 +++++++++++ S7.Net/PlcSynchronous.cs | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/S7.Net/PlcAsynchronous.cs b/S7.Net/PlcAsynchronous.cs index eb49e5d1..63edcd92 100644 --- a/S7.Net/PlcAsynchronous.cs +++ b/S7.Net/PlcAsynchronous.cs @@ -312,6 +312,17 @@ public async Task> ReadMultipleVarsAsync(List dataItems 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) + { + throw new NotImplementedException(); + } + /// /// 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 1b3af97f..50483ada 100644 --- a/S7.Net/PlcSynchronous.cs +++ b/S7.Net/PlcSynchronous.cs @@ -492,6 +492,15 @@ public void ReadMultipleVars(List dataItems) } } + /// + /// Read the PLC clock value. + /// + /// The current PLC time. + public System.DateTime ReadClock() + { + throw new NotImplementedException(); + } + /// /// Read the current status from the PLC. A value of 0x08 indicates the PLC is in run status, regardless of the PLC type. /// From 13544a1bcf837d5142f5cdf62d59a84be57062e4 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Wed, 23 Aug 2023 23:03:45 +0200 Subject: [PATCH 02/13] test: Add ReadClock test --- .../CommunicationTests/ReadClock.cs | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 S7.Net.UnitTest/CommunicationTests/ReadClock.cs diff --git a/S7.Net.UnitTest/CommunicationTests/ReadClock.cs b/S7.Net.UnitTest/CommunicationTests/ReadClock.cs new file mode 100644 index 00000000..f2902b14 --- /dev/null +++ b/S7.Net.UnitTest/CommunicationTests/ReadClock.cs @@ -0,0 +1,130 @@ +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 ReadClock +{ + [TestMethod, Timeout(500)] + 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 07 00 + // 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 + 0a 00 00 00 + """, + """ + // TPKT + 03 00 00 2b + + // COTP + 02 f0 80 + + // S7 read clock response + // UserData header + 32 07 00 00 07 00 + // 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)); + } +} \ No newline at end of file From 5e1ac8c7bf1b21a21231281c1649530573a09c19 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Tue, 29 Aug 2023 19:42:59 +0200 Subject: [PATCH 03/13] test(ReadClock): Use template parameters for PDU bytes --- S7.Net.UnitTest/CommunicationTests/ReadClock.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/S7.Net.UnitTest/CommunicationTests/ReadClock.cs b/S7.Net.UnitTest/CommunicationTests/ReadClock.cs index f2902b14..55042d23 100644 --- a/S7.Net.UnitTest/CommunicationTests/ReadClock.cs +++ b/S7.Net.UnitTest/CommunicationTests/ReadClock.cs @@ -26,7 +26,7 @@ 02 f0 80 // S7 read clock // UserData header - 32 07 00 00 07 00 + 32 07 00 00 PDU1 PDU2 // Parameter length 00 08 // Data length @@ -58,7 +58,7 @@ 02 f0 80 // S7 read clock response // UserData header - 32 07 00 00 07 00 + 32 07 00 00 PDU1 PDU2 // Parameter length 00 0c // Data length From eada47cd24b015fa4bf014588d3a28c75e188e7b Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Tue, 29 Aug 2023 20:41:55 +0200 Subject: [PATCH 04/13] refactor(PLCHelpers): Extract WriteSzlRequestHeader --- S7.Net/PLCHelpers.cs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs index fa016726..e1a757c5 100644 --- a/S7.Net/PLCHelpers.cs +++ b/S7.Net/PLCHelpers.cs @@ -56,15 +56,13 @@ private static void WriteUserDataHeader(System.IO.MemoryStream stream, int param WriteS7Header(stream, s7MessageTypeUserData, parameterLength, dataLength); } - private static void WriteSzlReadRequest(System.IO.MemoryStream stream, ushort szlId, ushort szlIndex) + private static void WriteSzlRequestHeader(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 szlTypeRequest = 0x4; // Parameter head stream.Write(new byte[] { 0x00, 0x01, 0x12 }); @@ -73,11 +71,20 @@ private static void WriteSzlReadRequest(System.IO.MemoryStream stream, ushort sz // Method stream.WriteByte(szlMethodRequest); // Type / function group - stream.WriteByte(szlTypeRequest << 4 | szlFunctionGroupCpuFunctions); + stream.WriteByte((byte)(szlTypeRequest << 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; + + WriteSzlRequestHeader(stream, szlFunctionGroupCpuFunctions, subFunctionReadSzl, 8); // Data const byte success = 0xff; @@ -343,7 +350,7 @@ private static byte[] BuildReadRequestPackage(IList dataItems) private static byte[] BuildSzlReadRequestPackage(ushort szlId, ushort szlIndex) { var stream = new System.IO.MemoryStream(); - + WriteSzlReadRequest(stream, szlId, szlIndex); stream.SetLength(stream.Position); From 07325db2fafb1d6c58fea6201dfe30f7b3ad336d Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Tue, 29 Aug 2023 21:34:09 +0200 Subject: [PATCH 05/13] feat: Implement clock reading --- S7.Net/Plc.Clock.cs | 53 +++++++++++++++++++++++++++++++++++++++ S7.Net/PlcAsynchronous.cs | 5 +++- S7.Net/PlcSynchronous.cs | 5 +++- S7.Net/Types/DateTime.cs | 5 ++++ 4 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 S7.Net/Plc.Clock.cs diff --git a/S7.Net/Plc.Clock.cs b/S7.Net/Plc.Clock.cs new file mode 100644 index 00000000..8b2e5bea --- /dev/null +++ b/S7.Net/Plc.Clock.cs @@ -0,0 +1,53 @@ +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 static byte[] BuildClockReadRequest() + { + var stream = new MemoryStream(); + + WriteSzlRequestHeader(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 pduErrOffset = 20; + const int dtResultOffset = pduErrOffset + 2; + const int dtLenOffset = dtResultOffset + 2; + const int dtValueOffset = dtLenOffset + 4; + + var pduErr = Word.FromByteArray(message.Skip(pduErrOffset).Take(2).ToArray()); + if (pduErr != 0) + { + throw new Exception($"Response from PLC indicates error 0x{pduErr:X4}."); + } + + var dtResult = message[dtResultOffset]; + if (dtResult != 0xff) + { + throw new Exception($"Response from PLC indicates error 0x{dtResult:X2}."); + } + + var len = Word.FromByteArray(message.Skip(dtLenOffset).Take(2).ToArray()); + if (len != Types.DateTime.Length) + { + throw new Exception($"Unexpected response length {len}, expected {Types.DateTime.Length}."); + } + + return Types.DateTime.FromByteArray(message.Skip(dtValueOffset).Take(Types.DateTime.Length).ToArray()); + } +} \ No newline at end of file diff --git a/S7.Net/PlcAsynchronous.cs b/S7.Net/PlcAsynchronous.cs index 63edcd92..2dcd058e 100644 --- a/S7.Net/PlcAsynchronous.cs +++ b/S7.Net/PlcAsynchronous.cs @@ -320,7 +320,10 @@ public async Task> ReadMultipleVarsAsync(List dataItems /// 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) { - throw new NotImplementedException(); + var request = BuildClockReadRequest(); + var response = await RequestTsduAsync(request, cancellationToken); + + return ParseClockReadResponse(response); } /// diff --git a/S7.Net/PlcSynchronous.cs b/S7.Net/PlcSynchronous.cs index 50483ada..d9537104 100644 --- a/S7.Net/PlcSynchronous.cs +++ b/S7.Net/PlcSynchronous.cs @@ -498,7 +498,10 @@ public void ReadMultipleVars(List dataItems) /// The current PLC time. public System.DateTime ReadClock() { - throw new NotImplementedException(); + var request = BuildClockReadRequest(); + var response = RequestTsdu(request); + + return ParseClockReadResponse(response); } /// diff --git a/S7.Net/Types/DateTime.cs b/S7.Net/Types/DateTime.cs index a685a210..993ca250 100644 --- a/S7.Net/Types/DateTime.cs +++ b/S7.Net/Types/DateTime.cs @@ -8,6 +8,11 @@ namespace S7.Net.Types /// public static class DateTime { + /// + /// The length in bytes of DateTime stored in the PLC. + /// + public const int Length = 10; + /// /// The minimum value supported by the specification. /// From 2f2dcf72819214c79f55ff01ff30e3f7f77ae099 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Tue, 29 Aug 2023 21:38:01 +0200 Subject: [PATCH 06/13] test: Raise ReadClock timeout to 1 second --- S7.Net.UnitTest/CommunicationTests/ReadClock.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/S7.Net.UnitTest/CommunicationTests/ReadClock.cs b/S7.Net.UnitTest/CommunicationTests/ReadClock.cs index 55042d23..23817831 100644 --- a/S7.Net.UnitTest/CommunicationTests/ReadClock.cs +++ b/S7.Net.UnitTest/CommunicationTests/ReadClock.cs @@ -9,7 +9,7 @@ namespace S7.Net.UnitTest.CommunicationTests; [TestClass] public class ReadClock { - [TestMethod, Timeout(500)] + [TestMethod, Timeout(1000)] public async Task Read_Clock_Value() { var cs = new CommunicationSequence From 1969aac1b2988cf0611b06aafe86874522ecfd19 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Wed, 30 Aug 2023 22:31:24 +0200 Subject: [PATCH 07/13] refactor: Rename WriteSzlRequestHeader to WriteUserDataRequest --- S7.Net/PLCHelpers.cs | 12 ++++++------ S7.Net/Plc.Clock.cs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs index e1a757c5..fc6bb141 100644 --- a/S7.Net/PLCHelpers.cs +++ b/S7.Net/PLCHelpers.cs @@ -56,22 +56,22 @@ private static void WriteUserDataHeader(System.IO.MemoryStream stream, int param WriteS7Header(stream, s7MessageTypeUserData, parameterLength, dataLength); } - private static void WriteSzlRequestHeader(System.IO.MemoryStream stream, byte functionGroup, byte subFunction, int dataLength) + private static void WriteUserDataRequest(System.IO.MemoryStream stream, byte functionGroup, byte subFunction, int dataLength) { WriteUserDataHeader(stream, 8, dataLength); // Parameter - const byte szlMethodRequest = 0x11; - const byte szlTypeRequest = 0x4; + 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((byte)(szlTypeRequest << 4 | (functionGroup & 0x0f))); + stream.WriteByte((byte)(userDataTypeRequest << 4 | (functionGroup & 0x0f))); // Subfunction stream.WriteByte(subFunction); // Sequence number @@ -84,7 +84,7 @@ private static void WriteSzlReadRequest(System.IO.MemoryStream stream, ushort sz const byte szlFunctionGroupCpuFunctions = 0b100; const byte subFunctionReadSzl = 0x01; - WriteSzlRequestHeader(stream, szlFunctionGroupCpuFunctions, subFunctionReadSzl, 8); + WriteUserDataRequest(stream, szlFunctionGroupCpuFunctions, subFunctionReadSzl, 8); // Data const byte success = 0xff; diff --git a/S7.Net/Plc.Clock.cs b/S7.Net/Plc.Clock.cs index 8b2e5bea..82394ced 100644 --- a/S7.Net/Plc.Clock.cs +++ b/S7.Net/Plc.Clock.cs @@ -16,7 +16,7 @@ private static byte[] BuildClockReadRequest() { var stream = new MemoryStream(); - WriteSzlRequestHeader(stream, SzlFunctionGroupTimers, SzlSubFunctionReadClock, 4); + WriteUserDataRequest(stream, SzlFunctionGroupTimers, SzlSubFunctionReadClock, 4); stream.Write(new byte[] { 0x0a, 0x00, 0x00, 0x00 }); stream.SetLength(stream.Position); From f419df4d73119975ddf0a5634a5f15cf6b195807 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Thu, 31 Aug 2023 19:55:41 +0200 Subject: [PATCH 08/13] feat: Stub PLC WriteClock methods --- S7.Net/PlcAsynchronous.cs | 12 ++++++++++++ S7.Net/PlcSynchronous.cs | 9 +++++++++ 2 files changed, 21 insertions(+) diff --git a/S7.Net/PlcAsynchronous.cs b/S7.Net/PlcAsynchronous.cs index 2dcd058e..86a9926a 100644 --- a/S7.Net/PlcAsynchronous.cs +++ b/S7.Net/PlcAsynchronous.cs @@ -326,6 +326,18 @@ public async Task> ReadMultipleVarsAsync(List dataItems 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) + { + throw new NotImplementedException(); + } + /// /// 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 d9537104..cfb4d998 100644 --- a/S7.Net/PlcSynchronous.cs +++ b/S7.Net/PlcSynchronous.cs @@ -504,6 +504,15 @@ public System.DateTime ReadClock() return ParseClockReadResponse(response); } + /// + /// Write the PLC clock value. + /// + /// The date and time to set the PLC clock to. + public void WriteClock(System.DateTime value) + { + throw new NotImplementedException(); + } + /// /// Read the current status from the PLC. A value of 0x08 indicates the PLC is in run status, regardless of the PLC type. /// From 1ebffe08e75311da84e8d01a2c7c3d7fe7062a20 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Thu, 31 Aug 2023 19:55:57 +0200 Subject: [PATCH 09/13] test: Add Write_Clock_Value test --- S7.Net.UnitTest/CommunicationTests/Clock.cs | 259 ++++++++++++++++++ .../CommunicationTests/ReadClock.cs | 130 --------- 2 files changed, 259 insertions(+), 130 deletions(-) create mode 100644 S7.Net.UnitTest/CommunicationTests/Clock.cs delete mode 100644 S7.Net.UnitTest/CommunicationTests/ReadClock.cs diff --git a/S7.Net.UnitTest/CommunicationTests/Clock.cs b/S7.Net.UnitTest/CommunicationTests/Clock.cs new file mode 100644 index 00000000..a7282add --- /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: read clock + 02 + // Sequence number + 01 + // Data unit reference + 00 + // Last data unit? Yes + 00 + // Error code + 00 00 + + // Data + // Error code + 0a + // Transport size: OCTET STRING + 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.UnitTest/CommunicationTests/ReadClock.cs b/S7.Net.UnitTest/CommunicationTests/ReadClock.cs deleted file mode 100644 index 23817831..00000000 --- a/S7.Net.UnitTest/CommunicationTests/ReadClock.cs +++ /dev/null @@ -1,130 +0,0 @@ -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 ReadClock -{ - [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 - 0a 00 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)); - } -} \ No newline at end of file From 0774e124bf1c7bc74c065e7065a56b7b24017bd2 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Mon, 4 Sep 2023 21:55:06 +0200 Subject: [PATCH 10/13] test: Fix comments in write clock messages --- S7.Net.UnitTest/CommunicationTests/Clock.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/S7.Net.UnitTest/CommunicationTests/Clock.cs b/S7.Net.UnitTest/CommunicationTests/Clock.cs index a7282add..7601aa8e 100644 --- a/S7.Net.UnitTest/CommunicationTests/Clock.cs +++ b/S7.Net.UnitTest/CommunicationTests/Clock.cs @@ -223,7 +223,7 @@ 00 01 12 12 // Type response (8...) Function group timers (...7) 87 - // Subfunction: read clock + // Subfunction: write clock 02 // Sequence number 01 @@ -237,7 +237,7 @@ 00 00 // Data // Error code 0a - // Transport size: OCTET STRING + // Transport size: NONE 00 // Length 00 00 From 10315b4b4c10967823c262acb5ad3f236ba10be2 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Mon, 4 Sep 2023 21:55:34 +0200 Subject: [PATCH 11/13] style: Add missing space in WriteClockAsync summary --- S7.Net/PlcAsynchronous.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/S7.Net/PlcAsynchronous.cs b/S7.Net/PlcAsynchronous.cs index 86a9926a..7a1dd44e 100644 --- a/S7.Net/PlcAsynchronous.cs +++ b/S7.Net/PlcAsynchronous.cs @@ -327,7 +327,7 @@ public async Task> ReadMultipleVarsAsync(List dataItems } /// - ///Write the PLC clock value. + /// 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. From cb24e9a04653451e24a263001753e350bdd681cd Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Mon, 4 Sep 2023 22:15:52 +0200 Subject: [PATCH 12/13] feat: Implement clock write support --- S7.Net/Plc.Clock.cs | 60 +++++++++++++++++++++++++++++---------- S7.Net/PlcAsynchronous.cs | 5 +++- S7.Net/PlcSynchronous.cs | 5 +++- 3 files changed, 53 insertions(+), 17 deletions(-) diff --git a/S7.Net/Plc.Clock.cs b/S7.Net/Plc.Clock.cs index 82394ced..45ccb364 100644 --- a/S7.Net/Plc.Clock.cs +++ b/S7.Net/Plc.Clock.cs @@ -11,6 +11,10 @@ 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; private static byte[] BuildClockReadRequest() { @@ -25,29 +29,55 @@ private static byte[] BuildClockReadRequest() private static DateTime ParseClockReadResponse(byte[] message) { - const int pduErrOffset = 20; - const int dtResultOffset = pduErrOffset + 2; - const int dtLenOffset = dtResultOffset + 2; - const int dtValueOffset = dtLenOffset + 4; + const int udLenOffset = UserDataResultOffset + 2; + const int udValueOffset = udLenOffset + 4; - var pduErr = Word.FromByteArray(message.Skip(pduErrOffset).Take(2).ToArray()); - if (pduErr != 0) + AssertPduResult(message); + AssertUserDataResult(message, 0xff); + + var len = Word.FromByteArray(message.Skip(udLenOffset).Take(2).ToArray()); + if (len != Types.DateTime.Length) { - throw new Exception($"Response from PLC indicates error 0x{pduErr:X4}."); + throw new Exception($"Unexpected response length {len}, expected {Types.DateTime.Length}."); } - var dtResult = message[dtResultOffset]; - if (dtResult != 0xff) + return Types.DateTime.FromByteArray(message.Skip(udValueOffset).Take(Types.DateTime.Length).ToArray()); + } + + private static byte[] BuildClockWriteRequest(DateTime value) + { + var stream = new MemoryStream(); + + WriteUserDataRequest(stream, SzlFunctionGroupTimers, SzlSubFunctionWriteClock, 14); + stream.Write(new byte[] { 0xff, TransportSizeOctetString, 0x00, Types.DateTime.Length }); + stream.Write(new byte[] { 0x00, 0x19 }); // Start of actual DateTime value, DateTime.ToByteArray is broken + 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{dtResult:X2}."); + throw new Exception($"Response from PLC indicates error 0x{pduErr:X4}."); } + } - var len = Word.FromByteArray(message.Skip(dtLenOffset).Take(2).ToArray()); - if (len != Types.DateTime.Length) + private static void AssertUserDataResult(byte[] message, byte expected) + { + var dtResult = message[UserDataResultOffset]; + if (dtResult != expected) { - throw new Exception($"Unexpected response length {len}, expected {Types.DateTime.Length}."); + throw new Exception($"Response from PLC was 0x{dtResult:X2}, expected 0x{expected:X2}."); } - - return Types.DateTime.FromByteArray(message.Skip(dtValueOffset).Take(Types.DateTime.Length).ToArray()); } } \ No newline at end of file diff --git a/S7.Net/PlcAsynchronous.cs b/S7.Net/PlcAsynchronous.cs index 7a1dd44e..8b828f3c 100644 --- a/S7.Net/PlcAsynchronous.cs +++ b/S7.Net/PlcAsynchronous.cs @@ -335,7 +335,10 @@ public async Task> ReadMultipleVarsAsync(List dataItems /// A task that represents the asynchronous operation. public async Task WriteClockAsync(System.DateTime value, CancellationToken cancellationToken = default) { - throw new NotImplementedException(); + var request = BuildClockWriteRequest(value); + var response = await RequestTsduAsync(request, cancellationToken); + + ParseClockWriteResponse(response); } /// diff --git a/S7.Net/PlcSynchronous.cs b/S7.Net/PlcSynchronous.cs index cfb4d998..2e281311 100644 --- a/S7.Net/PlcSynchronous.cs +++ b/S7.Net/PlcSynchronous.cs @@ -510,7 +510,10 @@ public System.DateTime ReadClock() /// The date and time to set the PLC clock to. public void WriteClock(System.DateTime value) { - throw new NotImplementedException(); + var request = BuildClockWriteRequest(value); + var response = RequestTsdu(request); + + ParseClockWriteResponse(response); } /// From 4764c997ed5068f79214a133b68916ae79652776 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Mon, 4 Sep 2023 22:20:31 +0200 Subject: [PATCH 13/13] refactor: Replace DateTime.Length with Plc.DateTimeLength in Plc.Clock --- S7.Net/Plc.Clock.cs | 21 +++++++++++++++------ S7.Net/Types/DateTime.cs | 5 ----- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/S7.Net/Plc.Clock.cs b/S7.Net/Plc.Clock.cs index 45ccb364..0aba639e 100644 --- a/S7.Net/Plc.Clock.cs +++ b/S7.Net/Plc.Clock.cs @@ -16,6 +16,11 @@ partial class Plc 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(); @@ -30,18 +35,21 @@ private static byte[] BuildClockReadRequest() private static DateTime ParseClockReadResponse(byte[] message) { const int udLenOffset = UserDataResultOffset + 2; - const int udValueOffset = udLenOffset + 4; + 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 != Types.DateTime.Length) + if (len != DateTimeLength) { - throw new Exception($"Unexpected response length {len}, expected {Types.DateTime.Length}."); + throw new Exception($"Unexpected response length {len}, expected {DateTimeLength}."); } - return Types.DateTime.FromByteArray(message.Skip(udValueOffset).Take(Types.DateTime.Length).ToArray()); + // 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) @@ -49,8 +57,9 @@ private static byte[] BuildClockWriteRequest(DateTime value) var stream = new MemoryStream(); WriteUserDataRequest(stream, SzlFunctionGroupTimers, SzlSubFunctionWriteClock, 14); - stream.Write(new byte[] { 0xff, TransportSizeOctetString, 0x00, Types.DateTime.Length }); - stream.Write(new byte[] { 0x00, 0x19 }); // Start of actual DateTime value, DateTime.ToByteArray is broken + 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); diff --git a/S7.Net/Types/DateTime.cs b/S7.Net/Types/DateTime.cs index 993ca250..a685a210 100644 --- a/S7.Net/Types/DateTime.cs +++ b/S7.Net/Types/DateTime.cs @@ -8,11 +8,6 @@ namespace S7.Net.Types /// public static class DateTime { - /// - /// The length in bytes of DateTime stored in the PLC. - /// - public const int Length = 10; - /// /// The minimum value supported by the specification. ///