diff --git a/S7.Net.UnitTest/CommunicationTests/ConnectionOpen.cs b/S7.Net.UnitTest/CommunicationTests/ConnectionOpen.cs new file mode 100644 index 00000000..15568d59 --- /dev/null +++ b/S7.Net.UnitTest/CommunicationTests/ConnectionOpen.cs @@ -0,0 +1,28 @@ +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 { + ConnectionOpenTemplates.ConnectionRequestConfirm, + ConnectionOpenTemplates.CommunicationSetup + }; + + 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 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/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 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..c3e44889 --- /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 void Add(RequestResponsePair requestResponsePair) + { + _requestResponsePairs.Add(requestResponsePair); + } + + 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 diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs index ef26e875..4109e536 100644 --- a/S7.Net/PLCHelpers.cs +++ b/S7.Net/PLCHelpers.cs @@ -9,25 +9,92 @@ 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(Word.ToByteArray((ushort) length)); + } + + 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.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)); + stream.Write(Word.ToByteArray((ushort) dataLength)); + } + /// - /// Creates the header to read bytes from the PLC + /// Creates the header to read bytes from the PLC. /// /// The stream to write to. /// The number of items to read. - 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 bytes - stream.Write(new byte[] { 0x03, 0x00 }); - //complete package size - stream.Write(Int.ToByteArray((short)(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)))); - stream.Write(new byte[] { 0x00, 0x00, 0x04 }); + // Header size 19, 12 bytes per item + WriteTpktHeader(stream, 19 + 12 * amount); + WriteDataHeader(stream); + WriteS7Header(stream, 0x01, 2 + 12 * amount, 0); + // Function code: read request + stream.WriteByte(0x04); //amount of requests 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); + } + + 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. @@ -253,7 +320,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) { @@ -262,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 77cd0c8b..eb49e5d1 100644 --- a/S7.Net/PlcAsynchronous.cs +++ b/S7.Net/PlcAsynchronous.cs @@ -312,6 +312,20 @@ 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. + /// + /// 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); + var s7data = await RequestTsduAsync(dataToSend, cancellationToken); + + return (byte) (s7data[37] & 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. diff --git a/S7.Net/PlcSynchronous.cs b/S7.Net/PlcSynchronous.cs index afd122af..1b3af97f 100644 --- a/S7.Net/PlcSynchronous.cs +++ b/S7.Net/PlcSynchronous.cs @@ -328,7 +328,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); @@ -473,7 +473,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) { @@ -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)