Merge pull request #491 from S7NetPlus/plc-status

Plc status
This commit is contained in:
Michael Croes
2023-08-02 19:36:58 +02:00
committed by GitHub
11 changed files with 481 additions and 13 deletions

View File

@@ -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));
}
}

View File

@@ -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)
"""
);
}

View File

@@ -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));
}
}

View File

@@ -0,0 +1,7 @@
using System.ComponentModel;
namespace System.Runtime.CompilerServices
{
[EditorBrowsable(EditorBrowsableState.Never)]
internal record IsExternalInit;
}

View File

@@ -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<RequestResponsePair>
{
private readonly List<RequestResponsePair> _requestResponsePairs = new List<RequestResponsePair>();
public void Add(RequestResponsePair requestResponsePair)
{
_requestResponsePairs.Add(requestResponsePair);
}
public void Add(string requestPattern, string responsePattern)
{
_requestResponsePairs.Add(new RequestResponsePair(requestPattern, responsePattern));
}
public IEnumerator<RequestResponsePair> 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<byte>.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<byte>.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;
}
}

View File

@@ -0,0 +1,3 @@
namespace S7.Net.UnitTest;
internal record RequestResponsePair(string RequestPattern, string ResponsePattern);

View File

@@ -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<string, byte>();
var res = new List<byte>();
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();
}
}

View File

@@ -8,6 +8,7 @@
</PropertyGroup>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile>
<IsPackable>false</IsPackable>

View File

@@ -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));
}
/// <summary>
/// Creates the header to read bytes from the PLC
/// Creates the header to read bytes from the PLC.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="amount">The number of items to read.</param>
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));
}
/// <summary>
/// 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 @@ namespace S7.Net
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 @@ namespace S7.Net
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();
}
}
}

View File

@@ -312,6 +312,20 @@ namespace S7.Net
return dataItems;
}
/// <summary>
/// Read the current status from the PLC. A value of 0x08 indicates the PLC is in run status, regardless of the PLC type.
/// </summary>
/// <param name="cancellationToken">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.</param>
/// <returns>A task that represents the asynchronous operation, with it's result set to the current PLC status on completion.</returns>
public async Task<byte> ReadStatusAsync(CancellationToken cancellationToken = default)
{
var dataToSend = BuildSzlReadRequestPackage(0x0424, 0);
var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
return (byte) (s7data[37] & 0x0f);
}
/// <summary>
/// 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.

View File

@@ -328,7 +328,7 @@ namespace S7.Net
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 @@ namespace S7.Net
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 @@ namespace S7.Net
}
}
/// <summary>
/// Read the current status from the PLC. A value of 0x08 indicates the PLC is in run status, regardless of the PLC type.
/// </summary>
/// <returns>The current PLC status.</returns>
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)