mirror of
https://github.com/S7NetPlus/s7netplus.git
synced 2026-02-26 01:28:29 +08:00
28
S7.Net.UnitTest/CommunicationTests/ConnectionOpen.cs
Normal file
28
S7.Net.UnitTest/CommunicationTests/ConnectionOpen.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
107
S7.Net.UnitTest/CommunicationTests/ConnectionOpenTemplates.cs
Normal file
107
S7.Net.UnitTest/CommunicationTests/ConnectionOpenTemplates.cs
Normal 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)
|
||||
"""
|
||||
);
|
||||
}
|
||||
57
S7.Net.UnitTest/CommunicationTests/ReadPlcStatus.cs
Normal file
57
S7.Net.UnitTest/CommunicationTests/ReadPlcStatus.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
7
S7.Net.UnitTest/Framework/IsExternalInit.cs
Normal file
7
S7.Net.UnitTest/Framework/IsExternalInit.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
internal record IsExternalInit;
|
||||
}
|
||||
82
S7.Net.UnitTest/Infrastructure/CommunicationSequence.cs
Normal file
82
S7.Net.UnitTest/Infrastructure/CommunicationSequence.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
3
S7.Net.UnitTest/Infrastructure/RequestResponsePair.cs
Normal file
3
S7.Net.UnitTest/Infrastructure/RequestResponsePair.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace S7.Net.UnitTest;
|
||||
|
||||
internal record RequestResponsePair(string RequestPattern, string ResponsePattern);
|
||||
80
S7.Net.UnitTest/Infrastructure/Responder.cs
Normal file
80
S7.Net.UnitTest/Infrastructure/Responder.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile>
|
||||
<IsPackable>false</IsPackable>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user