From 9c8b453326a14fe61e7f8bc34183d09db81f13f4 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Wed, 19 Jul 2023 23:18:26 +0200 Subject: [PATCH 01/21] refactor: Extract WriteTpktHeader --- S7.Net/PLCHelpers.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs index c0fbd78f..6e6f3069 100644 --- a/S7.Net/PLCHelpers.cs +++ b/S7.Net/PLCHelpers.cs @@ -9,6 +9,12 @@ namespace S7.Net { public partial class Plc { + private static void WriteTpktHeader(System.IO.MemoryStream stream, int length) + { + stream.Write(new byte[] { 0x03, 0x00 }); + stream.Write(Int.ToByteArray((short)length)); + } + /// /// Creates the header to read bytes from the PLC /// @@ -16,10 +22,9 @@ public partial class Plc /// private static void BuildHeaderPackage(System.IO.MemoryStream stream, int amount = 1) { - //header size = 19 bytes - stream.Write(new byte[] { 0x03, 0x00 }); - //complete package size - stream.Write(Int.ToByteArray((short)(19 + (12 * amount)))); + // Header size 19, 12 bytes per item + WriteTpktHeader(stream, 19 + (12 * amount)); + stream.Write(new byte[] { 0x02, 0xf0, 0x80, 0x32, 0x01, 0x00, 0x00, 0x00, 0x00 }); //data part size stream.Write(Word.ToByteArray((ushort)(2 + (amount * 12)))); From 42194aa7882883cbd1ea0e96b72c1e4a96baef23 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Wed, 19 Jul 2023 23:19:32 +0200 Subject: [PATCH 02/21] refactor: Extract WriteDataHeader --- S7.Net/PLCHelpers.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs index 6e6f3069..97623ccd 100644 --- a/S7.Net/PLCHelpers.cs +++ b/S7.Net/PLCHelpers.cs @@ -15,6 +15,11 @@ private static void WriteTpktHeader(System.IO.MemoryStream stream, int length) stream.Write(Int.ToByteArray((short)length)); } + private static void WriteDataHeader(System.IO.MemoryStream stream) + { + stream.Write(new byte[] { 0x02, 0xf0, 0x80 }); + } + /// /// Creates the header to read bytes from the PLC /// @@ -24,8 +29,8 @@ private static void BuildHeaderPackage(System.IO.MemoryStream stream, int amount { // Header size 19, 12 bytes per item WriteTpktHeader(stream, 19 + (12 * amount)); - - stream.Write(new byte[] { 0x02, 0xf0, 0x80, 0x32, 0x01, 0x00, 0x00, 0x00, 0x00 }); + WriteDateHeader(stream); + stream.Write(new byte[] { 0x32, 0x01, 0x00, 0x00, 0x00, 0x00 }); //data part size stream.Write(Word.ToByteArray((ushort)(2 + (amount * 12)))); stream.Write(new byte[] { 0x00, 0x00, 0x04 }); From ebf3da6280f1f60a1990bbc534b8bd945798f028 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Wed, 19 Jul 2023 23:29:11 +0200 Subject: [PATCH 03/21] refactor: Extract WriteS7Header --- S7.Net/PLCHelpers.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs index 97623ccd..ce5f6d2c 100644 --- a/S7.Net/PLCHelpers.cs +++ b/S7.Net/PLCHelpers.cs @@ -20,6 +20,16 @@ private static void WriteDataHeader(System.IO.MemoryStream stream) stream.Write(new byte[] { 0x02, 0xf0, 0x80 }); } + private static void WriteS7Header(System.IO.MemoryStream stream, byte messageType, int parameterLength, int dataLength) + { + stream.Write(0x32); // S7 protocol ID + stream.Write(messageType); // Message type + stream.Write(new byte[] { 0x00, 0x00 }); // Reserved + stream.Write(new byte[] { 0x00, 0x00 }); // PDU ref + stream.Write(Word.ToByteArray((ushort) parameterLength)); + stream.Write(Word.ToByteArray((ushort) dataLength)); + } + /// /// Creates the header to read bytes from the PLC /// @@ -30,10 +40,9 @@ private static void BuildHeaderPackage(System.IO.MemoryStream stream, int amount // Header size 19, 12 bytes per item WriteTpktHeader(stream, 19 + (12 * amount)); WriteDateHeader(stream); - stream.Write(new byte[] { 0x32, 0x01, 0x00, 0x00, 0x00, 0x00 }); - //data part size - stream.Write(Word.ToByteArray((ushort)(2 + (amount * 12)))); - stream.Write(new byte[] { 0x00, 0x00, 0x04 }); + WriteS7Header(stream, 0x01, 2 + amount * 12, 0); + // Function code: read request + stream.Write(0x04); //amount of requests stream.WriteByte((byte)amount); } From 296ead69c7899770e16bb2024576a40444988615 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Wed, 19 Jul 2023 23:30:02 +0200 Subject: [PATCH 04/21] refactor: Use Word.ToByteArray in WriteTpktHeader --- S7.Net/PLCHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs index ce5f6d2c..6b8e0ee1 100644 --- a/S7.Net/PLCHelpers.cs +++ b/S7.Net/PLCHelpers.cs @@ -12,7 +12,7 @@ public partial class Plc private static void WriteTpktHeader(System.IO.MemoryStream stream, int length) { stream.Write(new byte[] { 0x03, 0x00 }); - stream.Write(Int.ToByteArray((short)length)); + stream.Write(Word.ToByteArray((ushort) length)); } private static void WriteDataHeader(System.IO.MemoryStream stream) From 8becc562a8b17d8acc703efecf09bd27d0ce65c1 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Wed, 19 Jul 2023 23:32:00 +0200 Subject: [PATCH 05/21] refactor: Cleanup inline math in BuildHeaderPackage - Remove unnecessary parentheses - Use constant value first in multiplication --- S7.Net/PLCHelpers.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs index 6b8e0ee1..15c25d90 100644 --- a/S7.Net/PLCHelpers.cs +++ b/S7.Net/PLCHelpers.cs @@ -38,9 +38,9 @@ private static void WriteS7Header(System.IO.MemoryStream stream, byte messageTyp private static void BuildHeaderPackage(System.IO.MemoryStream stream, int amount = 1) { // Header size 19, 12 bytes per item - WriteTpktHeader(stream, 19 + (12 * amount)); + WriteTpktHeader(stream, 19 + 12 * amount); WriteDateHeader(stream); - WriteS7Header(stream, 0x01, 2 + amount * 12, 0); + WriteS7Header(stream, 0x01, 2 + 12 * amount, 0); // Function code: read request stream.Write(0x04); //amount of requests From cf94f8ad112737f8218055aec5bc6d0c6d16fc6c Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Thu, 20 Jul 2023 21:25:24 +0200 Subject: [PATCH 06/21] fix(PLCHelpers): Fix errors from refactors --- S7.Net/PLCHelpers.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs index 15c25d90..1f735035 100644 --- a/S7.Net/PLCHelpers.cs +++ b/S7.Net/PLCHelpers.cs @@ -22,8 +22,8 @@ private static void WriteDataHeader(System.IO.MemoryStream stream) private static void WriteS7Header(System.IO.MemoryStream stream, byte messageType, int parameterLength, int dataLength) { - stream.Write(0x32); // S7 protocol ID - stream.Write(messageType); // Message type + stream.WriteByte(0x32); // S7 protocol ID + stream.WriteByte(messageType); // Message type stream.Write(new byte[] { 0x00, 0x00 }); // Reserved stream.Write(new byte[] { 0x00, 0x00 }); // PDU ref stream.Write(Word.ToByteArray((ushort) parameterLength)); @@ -39,10 +39,10 @@ private static void BuildHeaderPackage(System.IO.MemoryStream stream, int amount { // Header size 19, 12 bytes per item WriteTpktHeader(stream, 19 + 12 * amount); - WriteDateHeader(stream); + WriteDataHeader(stream); WriteS7Header(stream, 0x01, 2 + 12 * amount, 0); // Function code: read request - stream.Write(0x04); + stream.WriteByte(0x04); //amount of requests stream.WriteByte((byte)amount); } From 38b26e0ce1e1cfebb73970fae3081593ec757ee1 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Thu, 20 Jul 2023 21:39:58 +0200 Subject: [PATCH 07/21] fix: Update test project target frameworks Ensures tests are actually run on GitHub --- S7.Net.UnitTest/S7.Net.UnitTest.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/S7.Net.UnitTest/S7.Net.UnitTest.csproj b/S7.Net.UnitTest/S7.Net.UnitTest.csproj index eb63e48c..2f932842 100644 --- a/S7.Net.UnitTest/S7.Net.UnitTest.csproj +++ b/S7.Net.UnitTest/S7.Net.UnitTest.csproj @@ -1,7 +1,7 @@  - net452;netcoreapp3.1;net5.0 + net462;net6.0 true Properties\S7.Net.snk From 7d212134e3b422173fa0d861ce76d5ae38b4148b Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Fri, 21 Jul 2023 21:23:00 +0200 Subject: [PATCH 08/21] refactor: Rename BuildHeaderPackage to WriteReadHeader --- S7.Net/PLCHelpers.cs | 4 ++-- S7.Net/PlcSynchronous.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs index 1f735035..110c71bb 100644 --- a/S7.Net/PLCHelpers.cs +++ b/S7.Net/PLCHelpers.cs @@ -35,7 +35,7 @@ private static void WriteS7Header(System.IO.MemoryStream stream, byte messageTyp /// /// /// - private static void BuildHeaderPackage(System.IO.MemoryStream stream, int amount = 1) + private static void WriteReadHeader(System.IO.MemoryStream stream, int amount = 1) { // Header size 19, 12 bytes per item WriteTpktHeader(stream, 19 + 12 * amount); @@ -271,7 +271,7 @@ private static byte[] BuildReadRequestPackage(IList dataItems) int packageSize = 19 + (dataItems.Count * 12); var package = new System.IO.MemoryStream(packageSize); - BuildHeaderPackage(package, dataItems.Count); + WriteReadHeader(package, dataItems.Count); foreach (var dataItem in dataItems) { diff --git a/S7.Net/PlcSynchronous.cs b/S7.Net/PlcSynchronous.cs index 4a3aaadb..7915e589 100644 --- a/S7.Net/PlcSynchronous.cs +++ b/S7.Net/PlcSynchronous.cs @@ -329,7 +329,7 @@ private void ReadBytesWithSingleRequest(DataType dataType, int db, int startByte const int packageSize = 19 + 12; // 19 header + 12 for 1 request var dataToSend = new byte[packageSize]; var package = new MemoryStream(dataToSend); - BuildHeaderPackage(package); + WriteReadHeader(package); // package.Add(0x02); // datenart BuildReadDataRequestPackage(package, dataType, db, startByteAdr, buffer.Length); @@ -474,7 +474,7 @@ public void ReadMultipleVars(List dataItems) int packageSize = 19 + (dataItems.Count * 12); var dataToSend = new byte[packageSize]; var package = new MemoryStream(dataToSend); - BuildHeaderPackage(package, dataItems.Count); + WriteReadHeader(package, dataItems.Count); // package.Add(0x02); // datenart foreach (var dataItem in dataItems) { From 1f26833244c30c30298007bbe1a0da9a9576eaad Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Fri, 21 Jul 2023 21:26:13 +0200 Subject: [PATCH 09/21] fix: Add missing xmldoc nodes in PLCHelpers --- S7.Net/PLCHelpers.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs index 110c71bb..d3c66d8a 100644 --- a/S7.Net/PLCHelpers.cs +++ b/S7.Net/PLCHelpers.cs @@ -31,10 +31,10 @@ private static void WriteS7Header(System.IO.MemoryStream stream, byte messageTyp } /// - /// Creates the header to read bytes from the PLC + /// Creates the header to read bytes from the PLC. /// - /// - /// + /// The to write to. + /// The amount of items to read. private static void WriteReadHeader(System.IO.MemoryStream stream, int amount = 1) { // Header size 19, 12 bytes per item @@ -51,6 +51,7 @@ private static void WriteReadHeader(System.IO.MemoryStream stream, int amount = /// Create the bytes-package to request data from the PLC. You have to specify the memory type (dataType), /// the address of the memory, the address of the byte and the bytes count. /// + /// The to write to. /// MemoryType (DB, Timer, Counter, etc.) /// Address of the memory to be read /// Start address of the byte From 18c3883dc0e37b7a1d2ddcd5ee505fde7d4f5587 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Fri, 21 Jul 2023 22:27:30 +0200 Subject: [PATCH 10/21] feat: Add WriteUserDataHeader --- S7.Net/PLCHelpers.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs index d3c66d8a..fc24a3a2 100644 --- a/S7.Net/PLCHelpers.cs +++ b/S7.Net/PLCHelpers.cs @@ -47,6 +47,15 @@ private static void WriteReadHeader(System.IO.MemoryStream stream, int amount = stream.WriteByte((byte)amount); } + private static void WriteUserDataHeader(System.IO.MemoryStream stream, int parameterLength, int dataLength) + { + const byte s7MessageTypeUserData = 0x07; + + WriteTpktHeader(stream, 17 + parameterLength + dataLength); + WriteDataHeader(stream); + WriteS7Header(stream, s7MessageTypeUserData, parameterLength, dataLength); + } + /// /// Create the bytes-package to request data from the PLC. You have to specify the memory type (dataType), /// the address of the memory, the address of the byte and the bytes count. From 1fc6899905a85f9412b9f1921b18ae8410bc4a8a Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Fri, 21 Jul 2023 22:28:22 +0200 Subject: [PATCH 11/21] feat: Add WriteSzlReadRequest --- S7.Net/PLCHelpers.cs | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs index fc24a3a2..bda60926 100644 --- a/S7.Net/PLCHelpers.cs +++ b/S7.Net/PLCHelpers.cs @@ -56,6 +56,45 @@ 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) + { + WriteUserDataHeader(stream, 8, 8); + + // Parameter + const byte szlMethodRequest = 0x11; + const byte szlTypeRequest = 0b100; + const byte szlFunctionGroupCpuFunctions = 0b100; + const byte subFunctionReadSzl = 0x01; + + // Parameter head + stream.Write(new byte[] { 0x00, 0x01, 0x12 }); + // Parameter length + stream.WriteByte(0x04); + // Method + stream.WriteByte(szlMethodRequest); + // Type / function group + stream.WriteByte(szlTypeRequest << 4 | szlFunctionGroupCpuFunctions); + // Subfunction + stream.WriteByte(subFunctionReadSzl); + // Sequence number + stream.WriteByte(0); + + // Data + const byte success = 0xff; + const byte transportSizeOctetString = 0x09; + + // Return code + stream.WriteByte(success); + // Transport size + stream.WriteByte(transportSizeOctetString); + // Length + stream.Write(Word.ToByteArray(4)); + // SZL-ID + stream.Write(Word.ToByteArray(szlId)); + // SZL-Index + stream.Write(Word.ToByteArray(szlIndex)); + } + /// /// Create the bytes-package to request data from the PLC. You have to specify the memory type (dataType), /// the address of the memory, the address of the byte and the bytes count. From 0d9ccea11b7d78e5f10b0cf4f6af3aa13a157982 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Sat, 22 Jul 2023 22:53:45 +0200 Subject: [PATCH 12/21] feat: Add Plc.ReadStatusAsync --- S7.Net/PLCHelpers.cs | 10 ++++++++++ S7.Net/PlcAsynchronous.cs | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs index bda60926..89156cc9 100644 --- a/S7.Net/PLCHelpers.cs +++ b/S7.Net/PLCHelpers.cs @@ -329,5 +329,15 @@ private static byte[] BuildReadRequestPackage(IList dataItems) return package.ToArray(); } + + private static byte[] BuildSzlReadRequestPackage(ushort szlId, ushort szlIndex) + { + var stream = new System.IO.MemoryStream(); + + WriteSzlReadRequest(stream, szlId, szlIndex); + stream.SetLength(stream.Position); + + return stream.ToArray(); + } } } diff --git a/S7.Net/PlcAsynchronous.cs b/S7.Net/PlcAsynchronous.cs index 36fef8c6..b29e738a 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 current status from the PLC. A value of 0x08 indicates the PLC is in run status, regardless of the PLC type. + /// + /// A task that represents the asynchronous operation, with it's result set to the current PLC status on completion. + public async Task ReadStatusAsync(CancellationToken cancellationToken) { + var dataToSend = BuildSzlReadRequestPackage(0x0424, 0); + var s7data = await RequestTsduAsync(dataToSend, cancellationToken); + + return (byte) (s7data[94] & 0x0f); + } + /// /// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests. /// If the write was not successful, check LastErrorCode or LastErrorString. From 714ac62ab1f92b9a200c1c0a3c30eff98239830f Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Mon, 31 Jul 2023 22:57:03 +0200 Subject: [PATCH 13/21] test: Add CommunicationSequence --- S7.Net.UnitTest/Framework/IsExternalInit.cs | 7 ++ .../Infrastructure/CommunicationSequence.cs | 82 +++++++++++++++++++ .../Infrastructure/RequestResponsePair.cs | 3 + S7.Net.UnitTest/Infrastructure/Responder.cs | 80 ++++++++++++++++++ S7.Net.UnitTest/S7.Net.UnitTest.csproj | 1 + 5 files changed, 173 insertions(+) create mode 100644 S7.Net.UnitTest/Framework/IsExternalInit.cs create mode 100644 S7.Net.UnitTest/Infrastructure/CommunicationSequence.cs create mode 100644 S7.Net.UnitTest/Infrastructure/RequestResponsePair.cs create mode 100644 S7.Net.UnitTest/Infrastructure/Responder.cs diff --git a/S7.Net.UnitTest/Framework/IsExternalInit.cs b/S7.Net.UnitTest/Framework/IsExternalInit.cs new file mode 100644 index 00000000..f70856c5 --- /dev/null +++ b/S7.Net.UnitTest/Framework/IsExternalInit.cs @@ -0,0 +1,7 @@ +using System.ComponentModel; + +namespace System.Runtime.CompilerServices +{ + [EditorBrowsable(EditorBrowsableState.Never)] + internal record IsExternalInit; +} \ No newline at end of file diff --git a/S7.Net.UnitTest/Infrastructure/CommunicationSequence.cs b/S7.Net.UnitTest/Infrastructure/CommunicationSequence.cs new file mode 100644 index 00000000..429b346a --- /dev/null +++ b/S7.Net.UnitTest/Infrastructure/CommunicationSequence.cs @@ -0,0 +1,82 @@ +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace S7.Net.UnitTest; + +internal class CommunicationSequence : IEnumerable +{ + private readonly List _requestResponsePairs = new List(); + + public CommunicationSequence() + { + + } + + public void Add(string requestPattern, string responsePattern) + { + _requestResponsePairs.Add(new RequestResponsePair(requestPattern, responsePattern)); + } + + public IEnumerator GetEnumerator() + { + return _requestResponsePairs.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public Task Serve(out int port) + { + var socket = CreateBoundListenSocket(out port); + socket.Listen(0); + + async Task Impl() + { + await Task.Yield(); + var socketIn = socket.Accept(); + + var buffer = ArrayPool.Shared.Rent(1024); + try + { + foreach (var pair in _requestResponsePairs) + { + var bytesReceived = socketIn.Receive(buffer, SocketFlags.None); + + var received = buffer.Take(bytesReceived).ToArray(); + Console.WriteLine($"=> {BitConverter.ToString(received)}"); + + var response = Responder.Respond(pair, received); + + Console.WriteLine($"<= {BitConverter.ToString(response)}"); + socketIn.Send(response); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + + socketIn.Close(); + } + + return Impl(); + } + + private static Socket CreateBoundListenSocket(out int port) + { + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + var endpoint = new IPEndPoint(IPAddress.Loopback, 0); + + socket.Bind(endpoint); + + var localEndpoint = (IPEndPoint)socket.LocalEndPoint!; + port = localEndpoint.Port; + + return socket; + } +} diff --git a/S7.Net.UnitTest/Infrastructure/RequestResponsePair.cs b/S7.Net.UnitTest/Infrastructure/RequestResponsePair.cs new file mode 100644 index 00000000..390ee62c --- /dev/null +++ b/S7.Net.UnitTest/Infrastructure/RequestResponsePair.cs @@ -0,0 +1,3 @@ +namespace S7.Net.UnitTest; + +internal record RequestResponsePair(string RequestPattern, string ResponsePattern); diff --git a/S7.Net.UnitTest/Infrastructure/Responder.cs b/S7.Net.UnitTest/Infrastructure/Responder.cs new file mode 100644 index 00000000..0fa15ea8 --- /dev/null +++ b/S7.Net.UnitTest/Infrastructure/Responder.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; + +namespace S7.Net.UnitTest; + +internal static class Responder +{ + private const string Comment = "//"; + private static char[] Space = " ".ToCharArray(); + + public static byte[] Respond(RequestResponsePair pair, byte[] request) + { + var offset = 0; + var matches = new Dictionary(); + var res = new List(); + using var requestReader = new StringReader(pair.RequestPattern); + + string line; + while ((line = requestReader.ReadLine()) != null) + { + var tokens = line.Split(Space, StringSplitOptions.RemoveEmptyEntries); + foreach (var token in tokens) + { + if (token.StartsWith(Comment)) break; + + if (offset >= request.Length) + { + throw new Exception("Request pattern has more data than request."); + } + + var received = request[offset]; + + if (token.Length == 2 && byte.TryParse(token, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value)) + { + // Number, exact match + if (value != received) + { + throw new Exception($"Incorrect data at offset {offset}. Expected {value:X2}, received {received:X2}."); + } + } + else + { + matches[token] = received; + } + + offset++; + } + } + + if (offset != request.Length) throw new Exception("Request contained more data than request pattern."); + + using var responseReader = new StringReader(pair.ResponsePattern); + while ((line = responseReader.ReadLine()) != null) + { + var tokens = line.Split(Space, StringSplitOptions.RemoveEmptyEntries); + foreach (var token in tokens) + { + if (token.StartsWith(Comment)) break; + + if (token.Length == 2 && byte.TryParse(token, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value)) + { + res.Add(value); + } + else + { + if (!matches.TryGetValue(token, out var match)) + { + throw new Exception($"Unmatched token '{token}' in response."); + } + + res.Add(match); + } + } + } + + return res.ToArray(); + } +} \ No newline at end of file diff --git a/S7.Net.UnitTest/S7.Net.UnitTest.csproj b/S7.Net.UnitTest/S7.Net.UnitTest.csproj index 2b29986b..a22bf0df 100644 --- a/S7.Net.UnitTest/S7.Net.UnitTest.csproj +++ b/S7.Net.UnitTest/S7.Net.UnitTest.csproj @@ -8,6 +8,7 @@ + latest true Properties\S7.Net.snk false From 8b8ad134648c3644d2e498b687765333f9776f81 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Mon, 31 Jul 2023 23:58:15 +0200 Subject: [PATCH 14/21] test: Add ConnectionOpen communication test --- .../CommunicationTests/ConnectionOpen.cs | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 S7.Net.UnitTest/CommunicationTests/ConnectionOpen.cs diff --git a/S7.Net.UnitTest/CommunicationTests/ConnectionOpen.cs b/S7.Net.UnitTest/CommunicationTests/ConnectionOpen.cs new file mode 100644 index 00000000..d19f7c33 --- /dev/null +++ b/S7.Net.UnitTest/CommunicationTests/ConnectionOpen.cs @@ -0,0 +1,127 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using S7.Net.Protocol; + +namespace S7.Net.UnitTest.CommunicationTests; + +[TestClass] +public class ConnectionOpen +{ + [TestMethod] + public async Task Does_Not_Throw() + { + var cs = new CommunicationSequence { + { + """ + // TPKT + 03 // Version + 00 // Reserved + 00 16 // Length + + // CR + 11 // Number of bytes following + E0 // CR / Credit + 00 00 // Destination reference, unused + __ __ // Source reference, unused + 00 // Class / Option + + // Source TSAP + C1 // Parameter code + 02 // Parameter length + TSAP_SRC_CHAN // Channel + TSAP_SRC_POS // Position + + // Destination TSAP + C2 // Parameter code + 02 // Parameter length + TSAP_DEST_CHAN // Channel + TSAP_DEST_POS // Position + + // PDU Size parameter + C0 // Parameter code + 01 // Parameter length + 0A // 1024 byte PDU (2 ^ 10) + """, + """ + // TPKT + 03 // Version + 00 // Reserved + 00 0B // Length + + // CC + 06 // Length + D0 // CC / Credit + 00 00 // Destination reference + 00 00 // Source reference + 00 // Class / Option + """ + }, + { + """ + // TPKT + 03 // Version + 00 // Reserved + 00 19 // Length + + // Data header + 02 // Length + F0 // Data identifier + 80 // PDU number and end of transmission + + // S7 header + 32 // Protocol ID + 01 // Message type job request + 00 00 // Reserved + PDU1 PDU2 // PDU reference + 00 08 // Parameter length (Communication Setup) + 00 00 // Data length + + // Communication Setup + F0 // Function code + 00 // Reserved + 00 03 // Max AMQ caller + 00 03 // Max AMQ callee + 03 C0 // PDU size (960) + """, + """ + // TPKT + 03 // Version + 00 // Reserved + 00 1B // Length + + // Data header + 02 // Length + F0 // Data identifier + 80 // PDU number and end of transmission + + // S7 header + 32 // Protocol ID + 03 // Message type ack data + 00 00 // Reserved + PDU1 PDU2 // PDU reference + 00 08 // Parameter length (Communication Setup) + 00 00 // Data length + 00 // Error class + 00 // Error code + + // Communication Setup + F0 // Function code + 00 // Reserved + 00 03 // Max AMQ caller + 00 03 // Max AMQ callee + 03 C0 // PDU size (960) + """ + } + }; + + 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(); + conn.Close(); + } + + await Task.WhenAll(cs.Serve(out var port), Client(port)); + } +} \ No newline at end of file From 54dadec75a35c1939ec54f36a1398ec3bd4fe9bd Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Tue, 1 Aug 2023 22:50:03 +0200 Subject: [PATCH 15/21] test: Extract connection open templates --- .../CommunicationTests/ConnectionOpen.cs | 103 +---------------- .../ConnectionOpenTemplates.cs | 107 ++++++++++++++++++ .../Infrastructure/CommunicationSequence.cs | 4 +- 3 files changed, 111 insertions(+), 103 deletions(-) create mode 100644 S7.Net.UnitTest/CommunicationTests/ConnectionOpenTemplates.cs diff --git a/S7.Net.UnitTest/CommunicationTests/ConnectionOpen.cs b/S7.Net.UnitTest/CommunicationTests/ConnectionOpen.cs index d19f7c33..15568d59 100644 --- a/S7.Net.UnitTest/CommunicationTests/ConnectionOpen.cs +++ b/S7.Net.UnitTest/CommunicationTests/ConnectionOpen.cs @@ -12,107 +12,8 @@ public class ConnectionOpen public async Task Does_Not_Throw() { var cs = new CommunicationSequence { - { - """ - // TPKT - 03 // Version - 00 // Reserved - 00 16 // Length - - // CR - 11 // Number of bytes following - E0 // CR / Credit - 00 00 // Destination reference, unused - __ __ // Source reference, unused - 00 // Class / Option - - // Source TSAP - C1 // Parameter code - 02 // Parameter length - TSAP_SRC_CHAN // Channel - TSAP_SRC_POS // Position - - // Destination TSAP - C2 // Parameter code - 02 // Parameter length - TSAP_DEST_CHAN // Channel - TSAP_DEST_POS // Position - - // PDU Size parameter - C0 // Parameter code - 01 // Parameter length - 0A // 1024 byte PDU (2 ^ 10) - """, - """ - // TPKT - 03 // Version - 00 // Reserved - 00 0B // Length - - // CC - 06 // Length - D0 // CC / Credit - 00 00 // Destination reference - 00 00 // Source reference - 00 // Class / Option - """ - }, - { - """ - // TPKT - 03 // Version - 00 // Reserved - 00 19 // Length - - // Data header - 02 // Length - F0 // Data identifier - 80 // PDU number and end of transmission - - // S7 header - 32 // Protocol ID - 01 // Message type job request - 00 00 // Reserved - PDU1 PDU2 // PDU reference - 00 08 // Parameter length (Communication Setup) - 00 00 // Data length - - // Communication Setup - F0 // Function code - 00 // Reserved - 00 03 // Max AMQ caller - 00 03 // Max AMQ callee - 03 C0 // PDU size (960) - """, - """ - // TPKT - 03 // Version - 00 // Reserved - 00 1B // Length - - // Data header - 02 // Length - F0 // Data identifier - 80 // PDU number and end of transmission - - // S7 header - 32 // Protocol ID - 03 // Message type ack data - 00 00 // Reserved - PDU1 PDU2 // PDU reference - 00 08 // Parameter length (Communication Setup) - 00 00 // Data length - 00 // Error class - 00 // Error code - - // Communication Setup - F0 // Function code - 00 // Reserved - 00 03 // Max AMQ caller - 00 03 // Max AMQ callee - 03 C0 // PDU size (960) - """ - } + ConnectionOpenTemplates.ConnectionRequestConfirm, + ConnectionOpenTemplates.CommunicationSetup }; async Task Client(int port) diff --git a/S7.Net.UnitTest/CommunicationTests/ConnectionOpenTemplates.cs b/S7.Net.UnitTest/CommunicationTests/ConnectionOpenTemplates.cs new file mode 100644 index 00000000..cb9cc0ad --- /dev/null +++ b/S7.Net.UnitTest/CommunicationTests/ConnectionOpenTemplates.cs @@ -0,0 +1,107 @@ +namespace S7.Net.UnitTest.CommunicationTests; + +internal static class ConnectionOpenTemplates +{ + public static RequestResponsePair ConnectionRequestConfirm { get; } = new RequestResponsePair( + """ + // TPKT + 03 // Version + 00 // Reserved + 00 16 // Length + + // CR + 11 // Number of bytes following + E0 // CR / Credit + 00 00 // Destination reference, unused + __ __ // Source reference, unused + 00 // Class / Option + + // Source TSAP + C1 // Parameter code + 02 // Parameter length + TSAP_SRC_CHAN // Channel + TSAP_SRC_POS // Position + + // Destination TSAP + C2 // Parameter code + 02 // Parameter length + TSAP_DEST_CHAN // Channel + TSAP_DEST_POS // Position + + // PDU Size parameter + C0 // Parameter code + 01 // Parameter length + 0A // 1024 byte PDU (2 ^ 10) + """, + """ + // TPKT + 03 // Version + 00 // Reserved + 00 0B // Length + + // CC + 06 // Length + D0 // CC / Credit + 00 00 // Destination reference + 00 00 // Source reference + 00 // Class / Option + """ + ); + + public static RequestResponsePair CommunicationSetup { get; } = new RequestResponsePair( + """ + // TPKT + 03 // Version + 00 // Reserved + 00 19 // Length + + // Data header + 02 // Length + F0 // Data identifier + 80 // PDU number and end of transmission + + // S7 header + 32 // Protocol ID + 01 // Message type job request + 00 00 // Reserved + PDU1 PDU2 // PDU reference + 00 08 // Parameter length (Communication Setup) + 00 00 // Data length + + // Communication Setup + F0 // Function code + 00 // Reserved + 00 03 // Max AMQ caller + 00 03 // Max AMQ callee + 03 C0 // PDU size (960) + """, + """ + // TPKT + 03 // Version + 00 // Reserved + 00 1B // Length + + // Data header + 02 // Length + F0 // Data identifier + 80 // PDU number and end of transmission + + // S7 header + 32 // Protocol ID + 03 // Message type ack data + 00 00 // Reserved + PDU1 PDU2 // PDU reference + 00 08 // Parameter length (Communication Setup) + 00 00 // Data length + 00 // Error class + 00 // Error code + + // Communication Setup + F0 // Function code + 00 // Reserved + 00 03 // Max AMQ caller + 00 03 // Max AMQ callee + 03 C0 // PDU size (960) + """ + ); +} \ No newline at end of file diff --git a/S7.Net.UnitTest/Infrastructure/CommunicationSequence.cs b/S7.Net.UnitTest/Infrastructure/CommunicationSequence.cs index 429b346a..c3e44889 100644 --- a/S7.Net.UnitTest/Infrastructure/CommunicationSequence.cs +++ b/S7.Net.UnitTest/Infrastructure/CommunicationSequence.cs @@ -13,9 +13,9 @@ internal class CommunicationSequence : IEnumerable { private readonly List _requestResponsePairs = new List(); - public CommunicationSequence() + public void Add(RequestResponsePair requestResponsePair) { - + _requestResponsePairs.Add(requestResponsePair); } public void Add(string requestPattern, string responsePattern) From 9b1faa01231fa9fa0020ccb42847068051b444f0 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Tue, 1 Aug 2023 22:50:50 +0200 Subject: [PATCH 16/21] test: Add test for reading PLC status --- .../CommunicationTests/ReadPlcStatus.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 S7.Net.UnitTest/CommunicationTests/ReadPlcStatus.cs diff --git a/S7.Net.UnitTest/CommunicationTests/ReadPlcStatus.cs b/S7.Net.UnitTest/CommunicationTests/ReadPlcStatus.cs new file mode 100644 index 00000000..13cc733b --- /dev/null +++ b/S7.Net.UnitTest/CommunicationTests/ReadPlcStatus.cs @@ -0,0 +1,57 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using S7.Net.Protocol; + +namespace S7.Net.UnitTest.CommunicationTests; + +[TestClass] +public class ReadPlcStatus +{ + [TestMethod] + public async Task Read_Status_Run() + { + var cs = new CommunicationSequence { + ConnectionOpenTemplates.ConnectionRequestConfirm, + ConnectionOpenTemplates.CommunicationSetup, + { + """ + // TPKT + 03 00 00 21 + + // COTP + 02 f0 80 + + // S7 SZL read + 32 07 00 00 PDU1 PDU2 00 08 00 08 00 01 12 04 11 44 + 01 00 ff 09 00 04 04 24 00 00 + """, + """ + // TPKT + 03 00 00 3d + + // COTP + 02 f0 80 + + // S7 SZL response + 32 07 00 00 PDU1 PDU2 00 0c 00 20 00 01 12 08 12 84 + 01 02 00 00 00 00 ff 09 00 1c 04 24 00 00 00 14 + 00 01 51 44 ff 08 00 00 00 00 00 00 00 00 14 08 + 20 12 05 28 34 94 + """ + } + }; + + 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 status = await conn.ReadStatusAsync(); + + Assert.AreEqual(0x08, status); + conn.Close(); + } + + await Task.WhenAll(cs.Serve(out var port), Client(port)); + } +} \ No newline at end of file From 97e27ccc2bc815686dabc6ab323984913cd726a4 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Tue, 1 Aug 2023 22:51:47 +0200 Subject: [PATCH 17/21] chore(ReadStatusAsync): Make cancellationToken optional --- 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 c280b0b4..726e6cb8 100644 --- a/S7.Net/PlcAsynchronous.cs +++ b/S7.Net/PlcAsynchronous.cs @@ -316,7 +316,7 @@ public async Task> ReadMultipleVarsAsync(List dataItems /// Read the current status from the PLC. A value of 0x08 indicates the PLC is in run status, regardless of the PLC type. /// /// A task that represents the asynchronous operation, with it's result set to the current PLC status on completion. - public async Task ReadStatusAsync(CancellationToken cancellationToken) { + public async Task ReadStatusAsync(CancellationToken cancellationToken = default) { var dataToSend = BuildSzlReadRequestPackage(0x0424, 0); var s7data = await RequestTsduAsync(dataToSend, cancellationToken); From e5823f280604ef11ed66911a43d30f45c2d3e315 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Tue, 1 Aug 2023 22:52:10 +0200 Subject: [PATCH 18/21] doc(ReadStatusAsync): Add missing cancellationToken documentation --- S7.Net/PlcAsynchronous.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/S7.Net/PlcAsynchronous.cs b/S7.Net/PlcAsynchronous.cs index 726e6cb8..526dd0ad 100644 --- a/S7.Net/PlcAsynchronous.cs +++ b/S7.Net/PlcAsynchronous.cs @@ -315,6 +315,8 @@ public async Task> ReadMultipleVarsAsync(List dataItems /// /// Read the current status from the PLC. A value of 0x08 indicates the PLC is in run status, regardless of the PLC type. /// + /// 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 status on completion. public async Task ReadStatusAsync(CancellationToken cancellationToken = default) { var dataToSend = BuildSzlReadRequestPackage(0x0424, 0); From c3934c3493278c5fecca79c5692b7f7ff2512133 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Tue, 1 Aug 2023 22:52:44 +0200 Subject: [PATCH 19/21] fix(ReadStatusAsync): Fix index of status in response message --- 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 526dd0ad..d7f0db05 100644 --- a/S7.Net/PlcAsynchronous.cs +++ b/S7.Net/PlcAsynchronous.cs @@ -322,7 +322,7 @@ public async Task ReadStatusAsync(CancellationToken cancellationToken = de var dataToSend = BuildSzlReadRequestPackage(0x0424, 0); var s7data = await RequestTsduAsync(dataToSend, cancellationToken); - return (byte) (s7data[94] & 0x0f); + return (byte) (s7data[37] & 0x0f); } /// From 970e9d43950f7ed7504b9ff69d2e76fb5c06c927 Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Tue, 1 Aug 2023 22:55:19 +0200 Subject: [PATCH 20/21] feat: Add sync version of ReadStatus --- S7.Net/PlcSynchronous.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/S7.Net/PlcSynchronous.cs b/S7.Net/PlcSynchronous.cs index c30e0cf7..1b3af97f 100644 --- a/S7.Net/PlcSynchronous.cs +++ b/S7.Net/PlcSynchronous.cs @@ -492,6 +492,18 @@ public void ReadMultipleVars(List dataItems) } } + /// + /// Read the current status from the PLC. A value of 0x08 indicates the PLC is in run status, regardless of the PLC type. + /// + /// The current PLC status. + public byte ReadStatus() + { + var dataToSend = BuildSzlReadRequestPackage(0x0424, 0); + var s7data = RequestTsdu(dataToSend); + + return (byte) (s7data[37] & 0x0f); + } + private byte[] RequestTsdu(byte[] requestData) => RequestTsdu(requestData, 0, requestData.Length); private byte[] RequestTsdu(byte[] requestData, int offset, int length) From addf6068bbf1d521ca690792b93b835c4d47e38e Mon Sep 17 00:00:00 2001 From: Michael Croes Date: Tue, 1 Aug 2023 22:56:08 +0200 Subject: [PATCH 21/21] style(ReadStatusAsync): Move opening brace to new line --- S7.Net/PlcAsynchronous.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/S7.Net/PlcAsynchronous.cs b/S7.Net/PlcAsynchronous.cs index d7f0db05..eb49e5d1 100644 --- a/S7.Net/PlcAsynchronous.cs +++ b/S7.Net/PlcAsynchronous.cs @@ -318,7 +318,8 @@ public async Task> ReadMultipleVarsAsync(List dataItems /// 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 status on completion. - public async Task ReadStatusAsync(CancellationToken cancellationToken = default) { + public async Task ReadStatusAsync(CancellationToken cancellationToken = default) + { var dataToSend = BuildSzlReadRequestPackage(0x0424, 0); var s7data = await RequestTsduAsync(dataToSend, cancellationToken);