diff --git a/S7.Net.UnitTest/CommunicationTests/Clock.cs b/S7.Net.UnitTest/CommunicationTests/Clock.cs new file mode 100644 index 00000000..7601aa8e --- /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 fa016726..fc6bb141 100644 --- a/S7.Net/PLCHelpers.cs +++ b/S7.Net/PLCHelpers.cs @@ -56,28 +56,35 @@ 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 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 @@ 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); diff --git a/S7.Net/Plc.Clock.cs b/S7.Net/Plc.Clock.cs new file mode 100644 index 00000000..0aba639e --- /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 eb49e5d1..8b828f3c 100644 --- a/S7.Net/PlcAsynchronous.cs +++ b/S7.Net/PlcAsynchronous.cs @@ -312,6 +312,35 @@ 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) + { + 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 1b3af97f..2e281311 100644 --- a/S7.Net/PlcSynchronous.cs +++ b/S7.Net/PlcSynchronous.cs @@ -492,6 +492,30 @@ public void ReadMultipleVars(List dataItems) } } + /// + /// 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. ///