56 Commits

Author SHA1 Message Date
Michael Croes
f1ae0ea084 Merge pull request #491 from S7NetPlus/plc-status
Plc status
2023-08-02 19:36:58 +02:00
Michael Croes
addf6068bb style(ReadStatusAsync): Move opening brace to new line 2023-08-01 22:56:08 +02:00
Michael Croes
970e9d4395 feat: Add sync version of ReadStatus 2023-08-01 22:55:19 +02:00
Michael Croes
c3934c3493 fix(ReadStatusAsync): Fix index of status in response message 2023-08-01 22:52:44 +02:00
Michael Croes
e5823f2806 doc(ReadStatusAsync): Add missing cancellationToken documentation 2023-08-01 22:52:10 +02:00
Michael Croes
97e27ccc2b chore(ReadStatusAsync): Make cancellationToken optional 2023-08-01 22:51:47 +02:00
Michael Croes
9b1faa0123 test: Add test for reading PLC status 2023-08-01 22:50:50 +02:00
Michael Croes
54dadec75a test: Extract connection open templates 2023-08-01 22:50:21 +02:00
Michael Croes
8b8ad13464 test: Add ConnectionOpen communication test 2023-07-31 23:58:15 +02:00
Michael Croes
714ac62ab1 test: Add CommunicationSequence 2023-07-31 23:57:38 +02:00
Michael Croes
088cd0a4a8 Merge branch 'main' into plc-status 2023-07-29 23:12:22 +02:00
Michael Croes
361db8be9d Merge pull request #494 from S7NetPlus/warnings
Cleanup of warnings
2023-07-29 23:07:23 +02:00
Michael Croes
e26860b0c0 build: Extend NoWarn
- Amend existing NoWarn if set
- Ignore out of support target framework warning
2023-07-28 23:57:55 +02:00
Michael Croes
6e103cea63 fix: Fix warnings in Struct 2023-07-28 23:55:12 +02:00
Michael Croes
c5023c10e4 style: Cleanup line endings in S7String 2023-07-28 23:54:15 +02:00
Michael Croes
b61ac32913 fix: Permit nulls in string ToByteArray conversions 2023-07-28 23:52:57 +02:00
Michael Croes
b27e1c9083 build: Set LangVersion to latest 2023-07-28 23:51:09 +02:00
Michael Croes
71f7f8b400 fix: Fix nullability warning in String.ToByteArray 2023-07-27 00:16:40 +02:00
Michael Croes
4aca9e4e53 fix: Fix remaining nullability warnings in Class 2023-07-27 00:11:18 +02:00
Michael Croes
0bb7c5351a ci: Update actions to Node 16 compatible versions 2023-07-26 23:59:21 +02:00
Michael Croes
c3f86c32a2 fix: Fix nullability warnings in Class.FromBytes 2023-07-26 23:52:28 +02:00
Michael Croes
3d0dd693ba fix: Fix nullability warnings in Class.ToBytes 2023-07-26 23:47:32 +02:00
Michael Croes
8ad25033d5 chore: Fix xmldoc warnings 2023-07-26 23:38:32 +02:00
Michael Croes
12e180ea2d build: Don't warn on missing xmldoc
While definitely desirable, at least temporarily disabled in order to
find other warnings.
2023-07-26 23:14:38 +02:00
Michael Croes
5891a30c5d Merge branch 'main' into plc-status 2023-07-25 23:25:54 +02:00
Michael Croes
b3077b27e7 Merge pull request #493 from S7NetPlus/testing
GitHub actions test improvements
2023-07-25 23:24:43 +02:00
Michael Croes
8126018afd test: Fix target framework*s* specification 2023-07-25 23:17:25 +02:00
Michael Croes
4e4071f07f test: Only target net462 on Windows 2023-07-24 22:04:18 +02:00
Michael Croes
534d9fd69d fix: Remove leftover test-framework in runner name 2023-07-24 21:41:32 +02:00
Michael Croes
8da292ad2f ci: Run tests against all target frameworks on all OS-es 2023-07-24 21:32:49 +02:00
Michael Croes
019aeb26dc Merge branch 'main' into plc-status
# Conflicts:
#	S7.Net.UnitTest/S7.Net.UnitTest.csproj
2023-07-23 23:34:03 +02:00
Michael Croes
670fb70b78 Merge pull request #492 from S7NetPlus/testing
Fix GH actions test runs
2023-07-23 23:29:46 +02:00
Michael Croes
aa15145184 fix: Install dotnet 7.x always 2023-07-23 23:20:18 +02:00
Michael Croes
12ea402769 fix: Remove separate restore step 2023-07-23 23:10:21 +02:00
Michael Croes
18402604d1 feat: Add net462, net6.0 and net7.0 targeting to S7NetPlus
This should be the actual baseline, which is also what the test project
targets now.
2023-07-23 23:04:03 +02:00
Michael Croes
53f651a482 fix: Constrain dotnet restore to matrix runtime 2023-07-23 22:58:53 +02:00
Michael Croes
7558b9a691 fix: Retarget test project to net462, net6.0 and net7.0
These are the frameworks currently used in the GitHub workflow, when
missing the tests aren't executed and the job will succeed nonetheless.
2023-07-23 22:53:11 +02:00
Michael Croes
3185d1fccf fix: Revert Ubuntu target back to 20.04 for snap7 ppa availability 2023-07-23 22:48:52 +02:00
Michael Croes
0d9ccea11b feat: Add Plc.ReadStatusAsync 2023-07-22 22:53:45 +02:00
Michael Croes
1fc6899905 feat: Add WriteSzlReadRequest 2023-07-21 22:28:22 +02:00
Michael Croes
18c3883dc0 feat: Add WriteUserDataHeader 2023-07-21 22:27:30 +02:00
Michael Croes
1f26833244 fix: Add missing xmldoc nodes in PLCHelpers 2023-07-21 21:26:13 +02:00
Michael Croes
7d212134e3 refactor: Rename BuildHeaderPackage to WriteReadHeader 2023-07-21 21:23:00 +02:00
Michael Croes
38b26e0ce1 fix: Update test project target frameworks
Ensures tests are actually run on GitHub
2023-07-20 21:39:58 +02:00
Michael Croes
cf94f8ad11 fix(PLCHelpers): Fix errors from refactors 2023-07-20 21:25:24 +02:00
Michael Croes
8becc562a8 refactor: Cleanup inline math in BuildHeaderPackage
- Remove unnecessary parentheses
- Use constant value first in multiplication
2023-07-19 23:32:00 +02:00
Michael Croes
296ead69c7 refactor: Use Word.ToByteArray in WriteTpktHeader 2023-07-19 23:30:02 +02:00
Michael Croes
ebf3da6280 refactor: Extract WriteS7Header 2023-07-19 23:29:11 +02:00
Michael Croes
42194aa788 refactor: Extract WriteDataHeader 2023-07-19 23:19:32 +02:00
Michael Croes
9c8b453326 refactor: Extract WriteTpktHeader 2023-07-19 23:18:26 +02:00
Michael Croes
e869d19587 Merge pull request #487 from mycroes/ci
Update ci workflow
2023-06-27 17:08:34 +02:00
Michael Croes
e7194bc470 Update OS, SDK and target versions 2023-06-27 17:00:25 +02:00
Michael Croes
5bc2c6c5e7 Add create_nuget and deploy jobs 2023-06-27 16:49:19 +02:00
Michael Croes
77dcb1778b Merge pull request #485 from dylandrush/484_Public_PLCExceptions
Made all exceptions public
2023-06-19 14:52:02 +02:00
DRUSH12
14053e342a Made all exceptions public 2023-06-19 06:14:14 -04:00
Michael Croes
ab3bd87701 chore: Delete GitVersion configuration
Rely on builtin defaults from now on.
2023-05-30 22:21:12 +02:00
25 changed files with 704 additions and 168 deletions

134
.github/workflows/dotnet.yml vendored Normal file
View File

@@ -0,0 +1,134 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: .NET
on:
workflow_dispatch: # Allow running the workflow manually from the GitHub UI
push:
branches:
- 'main' # Run the workflow when pushing to the main branch
pull_request:
branches:
- '*' # Run the workflow for all pull requests
release:
types:
- published # Run the workflow when a new GitHub release is published
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
DOTNET_NOLOGO: true
NuGetDirectory: ${{ github.workspace}}/nuget
defaults:
run:
shell: pwsh
jobs:
create_nuget:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Get all history to allow automatic versioning
- name: Install GitVersion
uses: gittools/actions/gitversion/setup@v0
with:
versionSpec: '6.x'
includePrerelease: true
preferLatestVersion: true
- name: Determine Version
id: gitversion
uses: gittools/actions/gitversion/execute@v0
- name: Setup .NET
uses: actions/setup-dotnet@v3
- run: >
dotnet pack
--configuration Release
/p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }}
/p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }}
/p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}
/p:PackageVersion=${{ steps.gitversion.outputs.semVer }}
--output ${{ env.NuGetDirectory }}
- uses: actions/upload-artifact@v3
with:
name: nuget
if-no-files-found: error
retention-days: 7
path: |
${{ env.NuGetDirectory }}/*.nupkg
${{ env.NuGetDirectory }}/*.snupkg
run_test:
name: test-${{ matrix.os }}
runs-on: ${{ matrix.os }}
env:
configuration: Release
artifacts: ${{ github.workspace }}/artifacts
strategy:
matrix:
os: [windows-latest, ubuntu-20.04, macos-latest]
fail-fast: false
steps:
- uses: actions/checkout@v3
- name: Install Snap7 Linux
if: ${{ matrix.os == 'ubuntu-20.04' }}
run: |
sudo add-apt-repository ppa:gijzelaar/snap7
sudo apt-get update
sudo apt-get install libsnap7-1 libsnap7-dev
- name: Install Snap7 MacOs
if: ${{ matrix.os == 'macos-latest' }}
run: |
brew install snap7
- name: Setup Dotnet
uses: actions/setup-dotnet@v3
with:
dotnet-version: |
6.x
7.x
- name: Nuget Cache
uses: actions/cache@v3
with:
path: ~/.nuget/packages
# Look to see if there is a cache hit for the corresponding requirements file
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
restore-keys: |
${{ runner.os }}-nuget
- name: Test
run: dotnet test --nologo --verbosity normal --logger GitHubActions
deploy:
# Publish only when creating a GitHub Release
# https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository
# You can update this logic if you want to manage releases differently
if: github.event_name == 'release'
runs-on: ubuntu-latest
needs: [ create_nuget, run_test ]
steps:
- uses: actions/download-artifact@v3
with:
name: nuget
path: ${{ env.NuGetDirectory }}
- name: Setup .NET Core
uses: actions/setup-dotnet@v3
# Publish all NuGet packages to NuGet.org
# Use --skip-duplicate to prevent errors if a package with the same version already exists.
# If you retry a failed workflow, already published packages will be skipped without error.
- name: Publish NuGet package
run: |
foreach($file in (Get-ChildItem "${{ env.NuGetDirectory }}" -Recurse -Include *.nupkg)) {
dotnet nuget push $file --api-key "${{ secrets.NUGET_APIKEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate
}

View File

@@ -1,80 +0,0 @@
name: Test
on:
[pull_request, push]
jobs:
build_test:
name: test-${{ matrix.os }}-${{ matrix.test-framework }}
runs-on: ${{ matrix.os }}
env:
configuration: Release
artifacts: ${{ github.workspace }}/artifacts
DOTNET_NOLOGO : 1
strategy:
matrix:
os: [windows-latest, ubuntu-20.04, macos-latest]
test-framework: [netcoreapp3.1, net5.0]
include:
- os: ubuntu-20.04
test-framework: netcoreapp3.1
installSnap7: true
dotnet-sdk: '3.1.x'
- os: ubuntu-20.04
test-framework: net5.0
installSnap7: true
dotnet-sdk: '5.0.x'
- os: macos-latest
test-framework: netcoreapp3.1
installSnap7: true
dotnet-sdk: '3.1.x'
- os: macos-latest
test-framework: net5.0
installSnap7: true
dotnet-sdk: '5.0.x'
- os: windows-latest
test-framework: netcoreapp3.1
dotnet-sdk: '3.1.x'
- os: windows-latest
test-framework: net5.0
dotnet-sdk: '5.0.x'
- os: windows-latest
test-framework: net452
dotnet-sdk: '5.0.x'
fail-fast: false
steps:
- uses: actions/checkout@v2
- name: Install Snap7 Linux
if: ${{ matrix.installSnap7 && matrix.os == 'ubuntu-20.04' }}
run: |
sudo add-apt-repository ppa:gijzelaar/snap7
sudo apt-get update
sudo apt-get install libsnap7-1 libsnap7-dev
- name: Install Snap7 MacOs
if: ${{ matrix.installSnap7 && matrix.os == 'macos-latest' }}
run: |
brew install snap7
- name: Setup Dotnet
uses: actions/setup-dotnet@v1
with:
dotnet-version: ${{ matrix.dotnet-sdk }}
- name: Nuget Cache
uses: actions/cache@v2
with:
path: ~/.nuget/packages
# 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') }}
restore-keys: |
${{ runner.os }}-${{ matrix.test-framework }}-nuget
- name: Restore
run: dotnet restore S7.Net.UnitTest
- name: Test
run: dotnet test --no-restore --nologo --verbosity normal --logger GitHubActions --framework ${{ matrix.test-framework }}

View File

@@ -1,21 +0,0 @@
assembly-informational-format: '{NuGetVersion}'
mode: ContinuousDeployment
branches:
master:
tag: rc
increment: Minor
feature:
regex: features?[/-]
tag: rc-{BranchName}
increment: Minor
pull-request:
regex: (pull|pull\-requests|pr)[/-]
tag: rc-pr-{BranchName}
increment: Minor
hotfix:
regex: hotfix(es)?[/-]
tag: rc
increment: Patch
develop:
regex: dev(elop)?(ment)?$
tag: b

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">
<PropertyGroup>
<TargetFrameworks>net452;netcoreapp3.1;net5.0</TargetFrameworks>
<PropertyGroup Condition=" '$(OS)' != 'Windows_NT' ">
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup Condition=" '$(OS)' == 'Windows_NT' ">
<TargetFrameworks>net462;net6.0;net7.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile>
<IsPackable>false</IsPackable>

View File

@@ -55,6 +55,7 @@ namespace S7.Net
/// See: https://tools.ietf.org/html/rfc905
/// </summary>
/// <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>
public static async Task<TPDU> ReadAsync(Stream stream, CancellationToken cancellationToken)
{
@@ -89,6 +90,7 @@ namespace S7.Net
/// See: https://tools.ietf.org/html/rfc905
/// </summary>
/// <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>
public static async Task<byte[]> ReadAsync(Stream stream, CancellationToken cancellationToken)
{

View File

@@ -6,7 +6,7 @@ using System.Runtime.Serialization;
namespace S7.Net
{
internal class WrongNumberOfBytesException : Exception
public class WrongNumberOfBytesException : Exception
{
public WrongNumberOfBytesException() : base()
{
@@ -27,7 +27,7 @@ namespace S7.Net
#endif
}
internal class InvalidAddressException : Exception
public class InvalidAddressException : Exception
{
public InvalidAddressException() : base ()
{
@@ -48,7 +48,7 @@ namespace S7.Net
#endif
}
internal class InvalidVariableTypeException : Exception
public class InvalidVariableTypeException : Exception
{
public InvalidVariableTypeException() : base()
{
@@ -69,7 +69,7 @@ namespace S7.Net
#endif
}
internal class TPKTInvalidException : Exception
public class TPKTInvalidException : Exception
{
public TPKTInvalidException() : base()
{
@@ -90,7 +90,7 @@ namespace S7.Net
#endif
}
internal class TPDUInvalidException : Exception
public class TPDUInvalidException : Exception
{
public TPDUInvalidException() : base()
{

View File

@@ -9,29 +9,97 @@ namespace S7.Net
{
public partial class Plc
{
/// <summary>
/// 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)
private static void WriteTpktHeader(System.IO.MemoryStream stream, int length)
{
//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 });
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.
/// </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
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.
/// </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="db">Address of the memory to be read</param>
/// <param name="startByteAdr">Start address of the byte</param>
@@ -252,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)
{
@@ -261,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.
@@ -428,7 +442,6 @@ namespace S7.Net
/// <summary>
/// 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>
/// <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>
@@ -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="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="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
/// <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)
{

View File

@@ -289,7 +289,6 @@ namespace S7.Net
/// <summary>
/// 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>
/// <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>
@@ -329,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);
@@ -474,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)
{
@@ -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, int offset, int length)

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<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>
<AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile>
<InternalsVisibleTo>S7.Net.UnitTest</InternalsVisibleTo>
@@ -15,19 +15,20 @@
<RepositoryType>git</RepositoryType>
<PackageTags>PLC Siemens Communication S7</PackageTags>
<Copyright>Derek Heiser 2015</Copyright>
<LangVersion>8.0</LangVersion>
<LangVersion>latest</LangVersion>
<Nullable>Enable</Nullable>
<DebugType>portable</DebugType>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591;NETSDK1138</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net452' Or '$(TargetFramework)' == 'netstandard2.0' ">
<PropertyGroup Condition="'$(TargetFramework)' == 'net452' Or '$(TargetFramework)' == 'net462' Or '$(TargetFramework)' == 'netstandard2.0' ">
<DefineConstants>NET_FULL</DefineConstants>
</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" />
</ItemGroup>

View File

@@ -39,6 +39,7 @@ namespace S7.Net
/// <param name="buffer">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="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
/// <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)
{

View File

@@ -29,6 +29,7 @@ namespace S7.Net
/// Reads a TPKT from the socket Async
/// </summary>
/// <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>
public static async Task<TPKT> ReadAsync(Stream stream, CancellationToken cancellationToken)
{

View File

@@ -64,7 +64,8 @@ namespace S7.Net.Types
numBytes += attribute.ReservedLengthInBytes;
break;
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);
break;
}
@@ -76,6 +77,8 @@ namespace S7.Net.Types
/// Gets the size of the class in bytes.
/// </summary>
/// <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>
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)
{
Type elementType = property.PropertyType.GetElementType();
Array array = (Array)property.GetValue(instance, null);
Type elementType = property.PropertyType.GetElementType()!;
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)
{
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;
break;
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);
value = propClass;
break;
@@ -213,6 +220,8 @@ namespace S7.Net.Types
/// </summary>
/// <param name="sourceClass">The object to fill in the given 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)
{
if (bytes == null)
@@ -223,9 +232,11 @@ namespace S7.Net.Types
{
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);
Type elementType = property.PropertyType.GetElementType();
Type elementType = property.PropertyType.GetElementType()!;
for (int i = 0; i < array.Length && numBytes < bytes.Length; i++)
{
array.SetValue(
@@ -320,26 +331,30 @@ namespace S7.Net.Types
/// <summary>
/// Creates a byte array depending on the struct type.
/// </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>
public static double ToBytes(object sourceClass, byte[] bytes, double numBytes = 0.0)
{
var properties = GetAccessableProperties(sourceClass.GetType());
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)
{
Array array = (Array)property.GetValue(sourceClass, null);
Array array = (Array) value;
IncrementToEven(ref numBytes);
Type elementType = property.PropertyType.GetElementType();
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
{
numBytes = SetBytesFromProperty(property.GetValue(sourceClass, null), property, bytes, numBytes);
numBytes = SetBytesFromProperty(value, property, bytes, 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.
/// </summary>
/// <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
/// <paramref name="dateTimes"/> is before <see cref="P:SpecMinimumDateTime"/>
/// 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
/// </summary>
public static class S7String
{
private static Encoding stringEncoding = Encoding.ASCII;
{
private static Encoding stringEncoding = Encoding.ASCII;
/// <summary>
/// The Encoding used when serializing and deserializing S7String (Encoding.ASCII by default)
/// </summary>
/// <exception cref="ArgumentNullException">StringEncoding must not be null</exception>
public static Encoding StringEncoding
{
get => stringEncoding;
set => stringEncoding = value ?? throw new ArgumentNullException(nameof(StringEncoding));
/// <exception cref="ArgumentNullException">StringEncoding must not be null</exception>
public static Encoding StringEncoding
{
get => stringEncoding;
set => stringEncoding = value ?? throw new ArgumentNullException(nameof(StringEncoding));
}
/// <summary>
@@ -58,7 +58,7 @@ namespace S7.Net.Types
/// <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>
/// <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)
{

View File

@@ -48,7 +48,7 @@ namespace S7.Net.Types
/// <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>
/// <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)
{

View File

@@ -12,13 +12,15 @@
/// <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)
{
var length = value?.Length;
if (length > reservedLength) length = 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;
}

View File

@@ -98,8 +98,8 @@ namespace S7.Net.Types
int bytePos = 0;
int bitPos = 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()
#if NETSTANDARD1_3
@@ -254,6 +254,14 @@ namespace S7.Net.Types
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;
switch (info.FieldType.Name)
{
@@ -261,7 +269,7 @@ namespace S7.Net.Types
// get the value
bytePos = (int)Math.Floor(numBytes);
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
else
bytes[bytePos] &= (byte)(~(byte)Math.Pow(2, bitPos)); // is false
@@ -270,26 +278,26 @@ namespace S7.Net.Types
case "Byte":
numBytes = (int)Math.Ceiling(numBytes);
bytePos = (int)numBytes;
bytes[bytePos] = (byte)info.GetValue(structValue);
bytes[bytePos] = GetValueOrThrow<byte>(info, structValue);
numBytes++;
break;
case "Int16":
bytes2 = Int.ToByteArray((Int16)info.GetValue(structValue));
bytes2 = Int.ToByteArray(GetValueOrThrow<short>(info, structValue));
break;
case "UInt16":
bytes2 = Word.ToByteArray((UInt16)info.GetValue(structValue));
bytes2 = Word.ToByteArray(GetValueOrThrow<ushort>(info, structValue));
break;
case "Int32":
bytes2 = DInt.ToByteArray((Int32)info.GetValue(structValue));
bytes2 = DInt.ToByteArray(GetValueOrThrow<int>(info, structValue));
break;
case "UInt32":
bytes2 = DWord.ToByteArray((UInt32)info.GetValue(structValue));
bytes2 = DWord.ToByteArray(GetValueOrThrow<uint>(info, structValue));
break;
case "Single":
bytes2 = Real.ToByteArray((float)info.GetValue(structValue));
bytes2 = Real.ToByteArray(GetValueOrThrow<float>(info, structValue));
break;
case "Double":
bytes2 = LReal.ToByteArray((double)info.GetValue(structValue));
bytes2 = LReal.ToByteArray(GetValueOrThrow<double>(info, structValue));
break;
case "String":
S7StringAttribute? attribute = info.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
@@ -298,8 +306,8 @@ namespace S7.Net.Types
bytes2 = attribute.Type switch
{
S7StringType.S7String => S7String.ToByteArray((string)info.GetValue(structValue), attribute.ReservedLength),
S7StringType.S7WString => S7WString.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),
_ => throw new ArgumentException("Please use a valid string type for the S7StringAttribute")
};
break;