Merge branch 'main' into timespan

This commit is contained in:
Michael Croes
2023-08-03 21:55:20 +02:00
committed by GitHub
22 changed files with 582 additions and 104 deletions

View File

@@ -64,77 +64,49 @@ jobs:
${{ env.NuGetDirectory }}/*.snupkg ${{ env.NuGetDirectory }}/*.snupkg
run_test: run_test:
name: test-${{ matrix.os }}-${{ matrix.test-framework }} name: test-${{ matrix.os }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
env: env:
configuration: Release configuration: Release
artifacts: ${{ github.workspace }}/artifacts artifacts: ${{ github.workspace }}/artifacts
strategy: strategy:
matrix: matrix:
os: [windows-latest, ubuntu-22.04, macos-latest] os: [windows-latest, ubuntu-20.04, macos-latest]
test-framework: [net6.0, net7.0]
include:
- os: ubuntu-22.04
test-framework: net6.0
installSnap7: true
dotnet-sdk: '6.x'
- os: ubuntu-22.04
test-framework: net7.0
installSnap7: true
dotnet-sdk: '7.x'
- os: macos-latest
test-framework: net6.0
installSnap7: true
dotnet-sdk: '6.x'
- os: macos-latest
test-framework: net7.0
installSnap7: true
dotnet-sdk: '7.x'
- os: windows-latest
test-framework: net6.0
dotnet-sdk: '6.x'
- os: windows-latest
test-framework: net7.0
dotnet-sdk: '7.x'
- os: windows-latest
test-framework: net462
dotnet-sdk: '7.x'
fail-fast: false fail-fast: false
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Install Snap7 Linux - name: Install Snap7 Linux
if: ${{ matrix.installSnap7 && matrix.os == 'ubuntu-20.04' }} if: ${{ matrix.os == 'ubuntu-20.04' }}
run: | run: |
sudo add-apt-repository ppa:gijzelaar/snap7 sudo add-apt-repository ppa:gijzelaar/snap7
sudo apt-get update sudo apt-get update
sudo apt-get install libsnap7-1 libsnap7-dev sudo apt-get install libsnap7-1 libsnap7-dev
- name: Install Snap7 MacOs - name: Install Snap7 MacOs
if: ${{ matrix.installSnap7 && matrix.os == 'macos-latest' }} if: ${{ matrix.os == 'macos-latest' }}
run: | run: |
brew install snap7 brew install snap7
- name: Setup Dotnet - name: Setup Dotnet
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: ${{ matrix.dotnet-sdk }} dotnet-version: |
6.x
7.x
- name: Nuget Cache - name: Nuget Cache
uses: actions/cache@v2 uses: actions/cache@v3
with: with:
path: ~/.nuget/packages path: ~/.nuget/packages
# Look to see if there is a cache hit for the corresponding requirements file # Look to see if there is a cache hit for the corresponding requirements file
key: ${{ runner.os }}-${{ matrix.test-framework }}-nuget-${{ hashFiles('**/packages.lock.json') }} key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
restore-keys: | restore-keys: |
${{ runner.os }}-${{ matrix.test-framework }}-nuget ${{ runner.os }}-nuget
- name: Restore
run: dotnet restore S7.Net.UnitTest
- name: Test - name: Test
run: dotnet test --no-restore --nologo --verbosity normal --logger GitHubActions --framework ${{ matrix.test-framework }} run: dotnet test --nologo --verbosity normal --logger GitHubActions
deploy: deploy:
# Publish only when creating a GitHub Release # Publish only when creating a GitHub Release

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

@@ -1,8 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup Condition=" '$(OS)' != 'Windows_NT' ">
<TargetFrameworks>net452;netcoreapp3.1;net5.0</TargetFrameworks> <TargetFrameworks>net6.0;net7.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup Condition=" '$(OS)' == 'Windows_NT' ">
<TargetFrameworks>net6.0;net7.0;net462</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<SignAssembly>true</SignAssembly> <SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile> <AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
@@ -11,7 +17,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="GitHubActionsTestLogger" Version="1.1.0" /> <PackageReference Include="GitHubActionsTestLogger" Version="2.3.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.2" /> <PackageReference Include="MSTest.TestAdapter" Version="2.1.2" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.2" /> <PackageReference Include="MSTest.TestFramework" Version="2.1.2" />

View File

@@ -55,6 +55,7 @@ namespace S7.Net
/// See: https://tools.ietf.org/html/rfc905 /// See: https://tools.ietf.org/html/rfc905
/// </summary> /// </summary>
/// <param name="stream">The socket to read from</param> /// <param name="stream">The socket to read from</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
/// <returns>COTP DPDU instance</returns> /// <returns>COTP DPDU instance</returns>
public static async Task<TPDU> ReadAsync(Stream stream, CancellationToken cancellationToken) public static async Task<TPDU> ReadAsync(Stream stream, CancellationToken cancellationToken)
{ {
@@ -89,6 +90,7 @@ namespace S7.Net
/// See: https://tools.ietf.org/html/rfc905 /// See: https://tools.ietf.org/html/rfc905
/// </summary> /// </summary>
/// <param name="stream">The stream to read from</param> /// <param name="stream">The stream to read from</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
/// <returns>Data in TSDU</returns> /// <returns>Data in TSDU</returns>
public static async Task<byte[]> ReadAsync(Stream stream, CancellationToken cancellationToken) public static async Task<byte[]> ReadAsync(Stream stream, CancellationToken cancellationToken)
{ {

View File

@@ -9,29 +9,97 @@ namespace S7.Net
{ {
public partial class Plc public partial class Plc
{ {
/// <summary> private static void WriteTpktHeader(System.IO.MemoryStream stream, int length)
/// Creates the header to read bytes from the PLC
/// </summary>
/// <param name="amount"></param>
/// <returns></returns>
private static void BuildHeaderPackage(System.IO.MemoryStream stream, int amount = 1)
{ {
//header size = 19 bytes
stream.Write(new byte[] { 0x03, 0x00 }); stream.Write(new byte[] { 0x03, 0x00 });
//complete package size stream.Write(Word.ToByteArray((ushort) length));
stream.Write(Int.ToByteArray((short)(19 + (12 * amount)))); }
stream.Write(new byte[] { 0x02, 0xf0, 0x80, 0x32, 0x01, 0x00, 0x00, 0x00, 0x00 });
//data part size private static void WriteDataHeader(System.IO.MemoryStream stream)
stream.Write(Word.ToByteArray((ushort)(2 + (amount * 12)))); {
stream.Write(new byte[] { 0x00, 0x00, 0x04 }); 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.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="amount">The number of items to read.</param>
private static void WriteReadHeader(System.IO.MemoryStream stream, int amount = 1)
{
// 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 //amount of requests
stream.WriteByte((byte)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);
}
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> /// <summary>
/// Create the bytes-package to request data from the PLC. You have to specify the memory type (dataType), /// 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 address of the memory, the address of the byte and the bytes count.
/// </summary> /// </summary>
/// <param name="stream">The stream to write the read data request to.</param>
/// <param name="dataType">MemoryType (DB, Timer, Counter, etc.)</param> /// <param name="dataType">MemoryType (DB, Timer, Counter, etc.)</param>
/// <param name="db">Address of the memory to be read</param> /// <param name="db">Address of the memory to be read</param>
/// <param name="startByteAdr">Start address of the byte</param> /// <param name="startByteAdr">Start address of the byte</param>
@@ -262,7 +330,7 @@ namespace S7.Net
int packageSize = 19 + (dataItems.Count * 12); int packageSize = 19 + (dataItems.Count * 12);
var package = new System.IO.MemoryStream(packageSize); var package = new System.IO.MemoryStream(packageSize);
BuildHeaderPackage(package, dataItems.Count); WriteReadHeader(package, dataItems.Count);
foreach (var dataItem in dataItems) foreach (var dataItem in dataItems)
{ {
@@ -271,5 +339,15 @@ namespace S7.Net
return package.ToArray(); 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; 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> /// <summary>
/// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests. /// 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. /// If the write was not successful, check LastErrorCode or LastErrorString.
@@ -428,7 +442,6 @@ namespace S7.Net
/// <summary> /// <summary>
/// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. /// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
/// If the write was not successful, check <see cref="LastErrorCode"/> or <see cref="LastErrorString"/>.
/// </summary> /// </summary>
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param> /// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
/// <param name="value">Value to be written to the PLC</param> /// <param name="value">Value to be written to the PLC</param>
@@ -507,6 +520,7 @@ namespace S7.Net
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param> /// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param> /// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.</param> /// <param name="value">Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous write operation.</returns> /// <returns>A task that represents the asynchronous write operation.</returns>
private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, ReadOnlyMemory<byte> value, CancellationToken cancellationToken) private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, ReadOnlyMemory<byte> value, CancellationToken cancellationToken)
{ {

View File

@@ -289,7 +289,6 @@ namespace S7.Net
/// <summary> /// <summary>
/// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. /// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
/// If the write was not successful, check <see cref="LastErrorCode"/> or <see cref="LastErrorString"/>.
/// </summary> /// </summary>
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param> /// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
/// <param name="value">Value to be written to the PLC</param> /// <param name="value">Value to be written to the PLC</param>
@@ -329,7 +328,7 @@ namespace S7.Net
const int packageSize = 19 + 12; // 19 header + 12 for 1 request const int packageSize = 19 + 12; // 19 header + 12 for 1 request
var dataToSend = new byte[packageSize]; var dataToSend = new byte[packageSize];
var package = new MemoryStream(dataToSend); var package = new MemoryStream(dataToSend);
BuildHeaderPackage(package); WriteReadHeader(package);
// package.Add(0x02); // datenart // package.Add(0x02); // datenart
BuildReadDataRequestPackage(package, dataType, db, startByteAdr, buffer.Length); BuildReadDataRequestPackage(package, dataType, db, startByteAdr, buffer.Length);
@@ -474,7 +473,7 @@ namespace S7.Net
int packageSize = 19 + (dataItems.Count * 12); int packageSize = 19 + (dataItems.Count * 12);
var dataToSend = new byte[packageSize]; var dataToSend = new byte[packageSize];
var package = new MemoryStream(dataToSend); var package = new MemoryStream(dataToSend);
BuildHeaderPackage(package, dataItems.Count); WriteReadHeader(package, dataItems.Count);
// package.Add(0x02); // datenart // package.Add(0x02); // datenart
foreach (var dataItem in dataItems) foreach (var dataItem in dataItems)
{ {
@@ -493,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) => RequestTsdu(requestData, 0, requestData.Length);
private byte[] RequestTsdu(byte[] requestData, int offset, int length) private byte[] RequestTsdu(byte[] requestData, int offset, int length)

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net452;netstandard2.0;netstandard1.3;net5.0</TargetFrameworks> <TargetFrameworks>net452;net462;netstandard2.0;netstandard1.3;net5.0;net6.0;net7.0</TargetFrameworks>
<SignAssembly>true</SignAssembly> <SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile> <AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile>
<InternalsVisibleTo>S7.Net.UnitTest</InternalsVisibleTo> <InternalsVisibleTo>S7.Net.UnitTest</InternalsVisibleTo>
@@ -15,19 +15,20 @@
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<PackageTags>PLC Siemens Communication S7</PackageTags> <PackageTags>PLC Siemens Communication S7</PackageTags>
<Copyright>Derek Heiser 2015</Copyright> <Copyright>Derek Heiser 2015</Copyright>
<LangVersion>8.0</LangVersion> <LangVersion>latest</LangVersion>
<Nullable>Enable</Nullable> <Nullable>Enable</Nullable>
<DebugType>portable</DebugType> <DebugType>portable</DebugType>
<IncludeSymbols>true</IncludeSymbols> <IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat> <SymbolPackageFormat>snupkg</SymbolPackageFormat>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591;NETSDK1138</NoWarn>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net452' Or '$(TargetFramework)' == 'netstandard2.0' "> <PropertyGroup Condition="'$(TargetFramework)' == 'net452' Or '$(TargetFramework)' == 'net462' Or '$(TargetFramework)' == 'netstandard2.0' ">
<DefineConstants>NET_FULL</DefineConstants> <DefineConstants>NET_FULL</DefineConstants>
</PropertyGroup> </PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' != 'net5.0'"> <ItemGroup Condition="'$(TargetFramework)' != 'net5.0' And '$(TargetFramework)' != 'net6.0' And '$(TargetFramework)' != 'net7.0'">
<PackageReference Include="System.Memory" Version="4.5.5" /> <PackageReference Include="System.Memory" Version="4.5.5" />
</ItemGroup> </ItemGroup>

View File

@@ -39,6 +39,7 @@ namespace S7.Net
/// <param name="buffer">the buffer to read into</param> /// <param name="buffer">the buffer to read into</param>
/// <param name="offset">the offset in the buffer to read into</param> /// <param name="offset">the offset in the buffer to read into</param>
/// <param name="count">the amount of bytes to read into the buffer</param> /// <param name="count">the amount of bytes to read into the buffer</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
/// <returns>returns the amount of read bytes</returns> /// <returns>returns the amount of read bytes</returns>
public static async Task<int> ReadExactAsync(this Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken) public static async Task<int> ReadExactAsync(this Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{ {

View File

@@ -29,6 +29,7 @@ namespace S7.Net
/// Reads a TPKT from the socket Async /// Reads a TPKT from the socket Async
/// </summary> /// </summary>
/// <param name="stream">The stream to read from</param> /// <param name="stream">The stream to read from</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
/// <returns>Task TPKT Instace</returns> /// <returns>Task TPKT Instace</returns>
public static async Task<TPKT> ReadAsync(Stream stream, CancellationToken cancellationToken) public static async Task<TPKT> ReadAsync(Stream stream, CancellationToken cancellationToken)
{ {

View File

@@ -64,7 +64,8 @@ namespace S7.Net.Types
numBytes += attribute.ReservedLengthInBytes; numBytes += attribute.ReservedLengthInBytes;
break; break;
default: default:
var propertyClass = Activator.CreateInstance(type); var propertyClass = Activator.CreateInstance(type) ??
throw new ArgumentException($"Failed to create instance of type {type}.", nameof(type));
numBytes = GetClassSize(propertyClass, numBytes, true); numBytes = GetClassSize(propertyClass, numBytes, true);
break; break;
} }
@@ -76,6 +77,8 @@ namespace S7.Net.Types
/// Gets the size of the class in bytes. /// Gets the size of the class in bytes.
/// </summary> /// </summary>
/// <param name="instance">An instance of the class</param> /// <param name="instance">An instance of the class</param>
/// <param name="numBytes">The offset of the current field.</param>
/// <param name="isInnerProperty"><see langword="true" /> if this property belongs to a class being serialized as member of the class requested for serialization; otherwise, <see langword="false" />.</param>
/// <returns>the number of bytes</returns> /// <returns>the number of bytes</returns>
public static double GetClassSize(object instance, double numBytes = 0.0, bool isInnerProperty = false) public static double GetClassSize(object instance, double numBytes = 0.0, bool isInnerProperty = false)
{ {
@@ -84,8 +87,10 @@ namespace S7.Net.Types
{ {
if (property.PropertyType.IsArray) if (property.PropertyType.IsArray)
{ {
Type elementType = property.PropertyType.GetElementType(); Type elementType = property.PropertyType.GetElementType()!;
Array array = (Array)property.GetValue(instance, null); Array array = (Array?) property.GetValue(instance, null) ??
throw new ArgumentException($"Property {property.Name} on {instance} must have a non-null value to get it's size.", nameof(instance));
if (array.Length <= 0) if (array.Length <= 0)
{ {
throw new Exception("Cannot determine size of class, because an array is defined which has no fixed size greater than zero."); throw new Exception("Cannot determine size of class, because an array is defined which has no fixed size greater than zero.");
@@ -199,7 +204,9 @@ namespace S7.Net.Types
numBytes += sData.Length; numBytes += sData.Length;
break; break;
default: default:
var propClass = Activator.CreateInstance(propertyType); var propClass = Activator.CreateInstance(propertyType) ??
throw new ArgumentException($"Failed to create instance of type {propertyType}.", nameof(propertyType));
numBytes = FromBytes(propClass, bytes, numBytes); numBytes = FromBytes(propClass, bytes, numBytes);
value = propClass; value = propClass;
break; break;
@@ -213,6 +220,8 @@ namespace S7.Net.Types
/// </summary> /// </summary>
/// <param name="sourceClass">The object to fill in the given array of bytes</param> /// <param name="sourceClass">The object to fill in the given array of bytes</param>
/// <param name="bytes">The array of bytes</param> /// <param name="bytes">The array of bytes</param>
/// <param name="numBytes">The offset for the current field.</param>
/// <param name="isInnerClass"><see langword="true" /> if this class is the type of a member of the class to be serialized; otherwise, <see langword="false" />.</param>
public static double FromBytes(object sourceClass, byte[] bytes, double numBytes = 0, bool isInnerClass = false) public static double FromBytes(object sourceClass, byte[] bytes, double numBytes = 0, bool isInnerClass = false)
{ {
if (bytes == null) if (bytes == null)
@@ -223,9 +232,11 @@ namespace S7.Net.Types
{ {
if (property.PropertyType.IsArray) if (property.PropertyType.IsArray)
{ {
Array array = (Array)property.GetValue(sourceClass, null); Array array = (Array?) property.GetValue(sourceClass, null) ??
throw new ArgumentException($"Property {property.Name} on sourceClass must be an array instance.", nameof(sourceClass));
IncrementToEven(ref numBytes); IncrementToEven(ref numBytes);
Type elementType = property.PropertyType.GetElementType(); Type elementType = property.PropertyType.GetElementType()!;
for (int i = 0; i < array.Length && numBytes < bytes.Length; i++) for (int i = 0; i < array.Length && numBytes < bytes.Length; i++)
{ {
array.SetValue( array.SetValue(
@@ -320,26 +331,30 @@ namespace S7.Net.Types
/// <summary> /// <summary>
/// Creates a byte array depending on the struct type. /// Creates a byte array depending on the struct type.
/// </summary> /// </summary>
/// <param name="sourceClass">The struct object</param> /// <param name="sourceClass">The struct object.</param>
/// <param name="bytes">The target byte array.</param>
/// <param name="numBytes">The offset for the current field.</param>
/// <returns>A byte array or null if fails.</returns> /// <returns>A byte array or null if fails.</returns>
public static double ToBytes(object sourceClass, byte[] bytes, double numBytes = 0.0) public static double ToBytes(object sourceClass, byte[] bytes, double numBytes = 0.0)
{ {
var properties = GetAccessableProperties(sourceClass.GetType()); var properties = GetAccessableProperties(sourceClass.GetType());
foreach (var property in properties) foreach (var property in properties)
{ {
var value = property.GetValue(sourceClass, null) ??
throw new ArgumentException($"Property {property.Name} on sourceClass can't be null.", nameof(sourceClass));
if (property.PropertyType.IsArray) if (property.PropertyType.IsArray)
{ {
Array array = (Array)property.GetValue(sourceClass, null); Array array = (Array) value;
IncrementToEven(ref numBytes); IncrementToEven(ref numBytes);
Type elementType = property.PropertyType.GetElementType();
for (int i = 0; i < array.Length && numBytes < bytes.Length; i++) for (int i = 0; i < array.Length && numBytes < bytes.Length; i++)
{ {
numBytes = SetBytesFromProperty(array.GetValue(i), property, bytes, numBytes); numBytes = SetBytesFromProperty(array.GetValue(i)!, property, bytes, numBytes);
} }
} }
else else
{ {
numBytes = SetBytesFromProperty(property.GetValue(sourceClass, null), property, bytes, numBytes); numBytes = SetBytesFromProperty(value, property, bytes, numBytes);
} }
} }
return numBytes; return numBytes;

View File

@@ -141,7 +141,7 @@ namespace S7.Net.Types
/// Converts an array of <see cref="T:System.DateTime"/> values to a byte array. /// Converts an array of <see cref="T:System.DateTime"/> values to a byte array.
/// </summary> /// </summary>
/// <param name="dateTimes">The DateTime values to convert.</param> /// <param name="dateTimes">The DateTime values to convert.</param>
/// <returns>A byte array containing the S7 date time representations of <paramref name="dateTime"/>.</returns> /// <returns>A byte array containing the S7 date time representations of <paramref name="dateTimes"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when any value of /// <exception cref="ArgumentOutOfRangeException">Thrown when any value of
/// <paramref name="dateTimes"/> is before <see cref="P:SpecMinimumDateTime"/> /// <paramref name="dateTimes"/> is before <see cref="P:SpecMinimumDateTime"/>
/// or after <see cref="P:SpecMaximumDateTime"/>.</exception> /// or after <see cref="P:SpecMaximumDateTime"/>.</exception>

View File

@@ -8,17 +8,17 @@ namespace S7.Net.Types
/// An S7 String has a preceeding 2 byte header containing its capacity and length /// An S7 String has a preceeding 2 byte header containing its capacity and length
/// </summary> /// </summary>
public static class S7String public static class S7String
{ {
private static Encoding stringEncoding = Encoding.ASCII; private static Encoding stringEncoding = Encoding.ASCII;
/// <summary> /// <summary>
/// The Encoding used when serializing and deserializing S7String (Encoding.ASCII by default) /// The Encoding used when serializing and deserializing S7String (Encoding.ASCII by default)
/// </summary> /// </summary>
/// <exception cref="ArgumentNullException">StringEncoding must not be null</exception> /// <exception cref="ArgumentNullException">StringEncoding must not be null</exception>
public static Encoding StringEncoding public static Encoding StringEncoding
{ {
get => stringEncoding; get => stringEncoding;
set => stringEncoding = value ?? throw new ArgumentNullException(nameof(StringEncoding)); set => stringEncoding = value ?? throw new ArgumentNullException(nameof(StringEncoding));
} }
/// <summary> /// <summary>
@@ -58,7 +58,7 @@ namespace S7.Net.Types
/// <param name="value">The string to convert to byte array.</param> /// <param name="value">The string to convert to byte array.</param>
/// <param name="reservedLength">The length (in characters) allocated in PLC for the string.</param> /// <param name="reservedLength">The length (in characters) allocated in PLC for the string.</param>
/// <returns>A <see cref="T:byte[]" /> containing the string header and string value with a maximum length of <paramref name="reservedLength"/> + 2.</returns> /// <returns>A <see cref="T:byte[]" /> containing the string header and string value with a maximum length of <paramref name="reservedLength"/> + 2.</returns>
public static byte[] ToByteArray(string value, int reservedLength) public static byte[] ToByteArray(string? value, int reservedLength)
{ {
if (value is null) if (value is null)
{ {

View File

@@ -48,7 +48,7 @@ namespace S7.Net.Types
/// <param name="value">The string to convert to byte array.</param> /// <param name="value">The string to convert to byte array.</param>
/// <param name="reservedLength">The length (in characters) allocated in PLC for the string.</param> /// <param name="reservedLength">The length (in characters) allocated in PLC for the string.</param>
/// <returns>A <see cref="T:byte[]" /> containing the string header and string value with a maximum length of <paramref name="reservedLength"/> + 4.</returns> /// <returns>A <see cref="T:byte[]" /> containing the string header and string value with a maximum length of <paramref name="reservedLength"/> + 4.</returns>
public static byte[] ToByteArray(string value, int reservedLength) public static byte[] ToByteArray(string? value, int reservedLength)
{ {
if (value is null) if (value is null)
{ {

View File

@@ -12,13 +12,15 @@
/// <param name="reservedLength">The amount of bytes reserved for the <paramref name="value"/> in the PLC.</param> /// <param name="reservedLength">The amount of bytes reserved for the <paramref name="value"/> in the PLC.</param>
public static byte[] ToByteArray(string value, int reservedLength) public static byte[] ToByteArray(string value, int reservedLength)
{ {
var length = value?.Length;
if (length > reservedLength) length = reservedLength;
var bytes = new byte[reservedLength]; var bytes = new byte[reservedLength];
if (value == null) return bytes;
if (length == null || length == 0) return bytes; var length = value.Length;
if (length == 0) return bytes;
System.Text.Encoding.ASCII.GetBytes(value, 0, length.Value, bytes, 0); if (length > reservedLength) length = reservedLength;
System.Text.Encoding.ASCII.GetBytes(value, 0, length, bytes, 0);
return bytes; return bytes;
} }

View File

@@ -99,8 +99,8 @@ namespace S7.Net.Types
int bytePos = 0; int bytePos = 0;
int bitPos = 0; int bitPos = 0;
double numBytes = 0.0; double numBytes = 0.0;
object structValue = Activator.CreateInstance(structType); object structValue = Activator.CreateInstance(structType) ??
throw new ArgumentException($"Failed to create an instance of the type {structType}.", nameof(structType));
var infos = structValue.GetType() var infos = structValue.GetType()
#if NETSTANDARD1_3 #if NETSTANDARD1_3
@@ -270,6 +270,14 @@ namespace S7.Net.Types
foreach (var info in infos) foreach (var info in infos)
{ {
static TValue GetValueOrThrow<TValue>(FieldInfo fi, object structValue) where TValue : struct
{
var value = fi.GetValue(structValue) as TValue? ??
throw new ArgumentException($"Failed to convert value of field {fi.Name} of {structValue} to type {typeof(TValue)}");
return value;
}
bytes2 = null; bytes2 = null;
switch (info.FieldType.Name) switch (info.FieldType.Name)
{ {
@@ -277,7 +285,7 @@ namespace S7.Net.Types
// get the value // get the value
bytePos = (int)Math.Floor(numBytes); bytePos = (int)Math.Floor(numBytes);
bitPos = (int)((numBytes - (double)bytePos) / 0.125); bitPos = (int)((numBytes - (double)bytePos) / 0.125);
if ((bool)info.GetValue(structValue)) if (GetValueOrThrow<bool>(info, structValue))
bytes[bytePos] |= (byte)Math.Pow(2, bitPos); // is true bytes[bytePos] |= (byte)Math.Pow(2, bitPos); // is true
else else
bytes[bytePos] &= (byte)(~(byte)Math.Pow(2, bitPos)); // is false bytes[bytePos] &= (byte)(~(byte)Math.Pow(2, bitPos)); // is false
@@ -286,26 +294,26 @@ namespace S7.Net.Types
case "Byte": case "Byte":
numBytes = (int)Math.Ceiling(numBytes); numBytes = (int)Math.Ceiling(numBytes);
bytePos = (int)numBytes; bytePos = (int)numBytes;
bytes[bytePos] = (byte)info.GetValue(structValue); bytes[bytePos] = GetValueOrThrow<byte>(info, structValue);
numBytes++; numBytes++;
break; break;
case "Int16": case "Int16":
bytes2 = Int.ToByteArray((Int16)info.GetValue(structValue)); bytes2 = Int.ToByteArray(GetValueOrThrow<short>(info, structValue));
break; break;
case "UInt16": case "UInt16":
bytes2 = Word.ToByteArray((UInt16)info.GetValue(structValue)); bytes2 = Word.ToByteArray(GetValueOrThrow<ushort>(info, structValue));
break; break;
case "Int32": case "Int32":
bytes2 = DInt.ToByteArray((Int32)info.GetValue(structValue)); bytes2 = DInt.ToByteArray(GetValueOrThrow<int>(info, structValue));
break; break;
case "UInt32": case "UInt32":
bytes2 = DWord.ToByteArray((UInt32)info.GetValue(structValue)); bytes2 = DWord.ToByteArray(GetValueOrThrow<uint>(info, structValue));
break; break;
case "Single": case "Single":
bytes2 = Real.ToByteArray((float)info.GetValue(structValue)); bytes2 = Real.ToByteArray(GetValueOrThrow<float>(info, structValue));
break; break;
case "Double": case "Double":
bytes2 = LReal.ToByteArray((double)info.GetValue(structValue)); bytes2 = LReal.ToByteArray(GetValueOrThrow<double>(info, structValue));
break; break;
case "String": case "String":
S7StringAttribute? attribute = info.GetCustomAttributes<S7StringAttribute>().SingleOrDefault(); S7StringAttribute? attribute = info.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
@@ -314,8 +322,8 @@ namespace S7.Net.Types
bytes2 = attribute.Type switch bytes2 = attribute.Type switch
{ {
S7StringType.S7String => S7String.ToByteArray((string)info.GetValue(structValue), attribute.ReservedLength), S7StringType.S7String => S7String.ToByteArray((string?)info.GetValue(structValue), attribute.ReservedLength),
S7StringType.S7WString => S7WString.ToByteArray((string)info.GetValue(structValue), attribute.ReservedLength), S7StringType.S7WString => S7WString.ToByteArray((string?)info.GetValue(structValue), attribute.ReservedLength),
_ => throw new ArgumentException("Please use a valid string type for the S7StringAttribute") _ => throw new ArgumentException("Please use a valid string type for the S7StringAttribute")
}; };
break; break;