62 Commits

Author SHA1 Message Date
Michael Croes
946536c2d6 Release S7NetPlus 0.13.0
Release highlights:
- Change default TSAP for S7 200
- Add S7 200 Smart support
- Add support for custom TSAP's
- Align data to even bytes when parsing responses from class and struct
  reads
- Close connection on IOException
- Add cancellation for Read/Write
- Set default Read-/WriteTimeout to 10 seconds
- Cleanup of sync helper methods
2021-06-21 21:52:25 +02:00
Michael Croes
b475aee2e7 Merge pull request #401 from scamille/test-ConnectionClose
Add unit test to ensure a connection has been properly closed
2021-06-10 17:04:42 +02:00
Serge Camille
82a745c972 Merge branch 'develop' into test-ConnectionClose 2021-06-08 22:53:30 +02:00
Serge Camille
74fecad48d Remove cancel timeouts. 2021-06-08 22:47:12 +02:00
Michael Croes
37c63cea8e Merge pull request #402 from mycroes/default-timeout
Default ReadTimeout and WriteTimeout to 10 seconds
2021-06-07 22:23:57 +02:00
Michael Croes
36c4564f5e Default ReadTimeout and WriteTimeout to 10 seconds 2021-06-07 22:19:23 +02:00
Michael Croes
0c0200d12d Merge pull request #400 from scamille/tests-taskCanceled
Fix Cancellation test.
2021-06-07 22:03:02 +02:00
Michael Croes
6e861d2f00 Merge branch 'develop' into tests-taskCanceled 2021-06-07 22:00:52 +02:00
Michael Croes
372f4a5dcb Merge pull request #398 from mycroes/cleanup-sync
Cleanup sync
2021-06-07 21:26:41 +02:00
Serge Camille
0d87dbf3c6 Add unit test to ensure a connection has been properly closed
If cancelled during data transmission, #349 adds code to ensure that the connection automatically gets cancelled.

Add inerted test to ensure that cancellation before sending data does not result in a cancellation.
2021-06-07 14:49:06 +02:00
Serge Camille
15f94cd7bf Fix Cancellation test. 2021-06-07 14:13:04 +02:00
Michael Croes
36b59a2926 Cleanup whitespace, usings 2021-06-06 23:20:19 +02:00
Michael Croes
8d081859da Remove unused sync methods 2021-06-06 23:19:56 +02:00
Michael Croes
2302819650 Merge pull request #349 from scamille/closeConnectionIOException
Close connection on IOExceptions.
2021-06-06 22:58:27 +02:00
Michael Croes
d65b83660f Merge branch 'develop' into closeConnectionIOException 2021-06-06 22:56:17 +02:00
Michael Croes
77b2ecfd45 Merge pull request #397 from scamille/multipleVarsAlignment
Fix ReadMultipleVars alignment bug
2021-06-06 22:51:03 +02:00
Michael Croes
b0a6a2375f Merge branch 'develop' into multipleVarsAlignment 2021-06-06 22:49:33 +02:00
Michael Croes
14823bca96 Merge pull request #395 from mycroes/s7-200-cr
Change S7 200 remote TSAP to 10.01
2021-06-06 22:45:53 +02:00
Serge Camille
53045c5952 Close connection on IOExceptions.
The idea behind this is that if we receive a response which does not conform to the underlying protocol specification (with all the TCP checks in place!), we are in a weird/corrupt state and it is better to just close the connection.
2021-06-06 22:40:04 +02:00
Michael Croes
677d2941e1 Apply S7 200 TSAP change to relocated code 2021-06-06 22:33:12 +02:00
Michael Croes
66b693676c Merge branch 'develop' into s7-200-cr 2021-06-06 22:32:41 +02:00
Michael Croes
4542bbedb2 Merge pull request #396 from mycroes/tsap
Refactor TSAP related code
2021-06-06 22:24:29 +02:00
Serge Camille
a4b6a360fe ParseDataIntoDataItems needs to always align the response address to a even value after each data item. 2021-06-06 22:16:59 +02:00
Serge Camille
0bef6bc9ff Add unit test for the problem. 2021-06-06 21:59:39 +02:00
Michael Croes
cbe04fbfb4 Merge branch 'develop' into tsap 2021-06-06 21:19:17 +02:00
Michael Croes
0b4c79cb08 Merge pull request #319 from scamille/githubActions2
GitHub actions
2021-06-06 21:14:26 +02:00
Serge Camille
5c24e801fd Add Actions Nuget Cache (#2)
* Try out action cache

* fix syntax error

* Now lets see what happens if it hits the cache
2021-06-05 18:50:28 +02:00
Serge Camille
bcde65120c Github Actions Net5.0 (#1)
* Try integrating dotnet setup version into build matrix.

* Another attempt

* change matrix.

* don't add dotnet-sdk as main matrix variable

* remove test framework as well.

* or maybe not

* Fix copy paste mixup
2021-06-05 18:37:35 +02:00
Serge Camille
4541a7ebb7 Try adjusting the dotnet setup version. 2021-06-05 18:11:06 +02:00
Serge Camille
fc9c33fdaf Add Net5.0 to test project as well. 2021-06-05 17:59:18 +02:00
Serge Camille
a23408d67e Add Net5.0 to test matrix 2021-06-05 17:58:05 +02:00
Serge Camille
adb55dc80f Merge branch 'develop' into githubActions2 2021-06-05 17:52:59 +02:00
Michael Croes
2fae2c01d5 Add TsapPair support to PLC
Add support for custom addressing by supplying a TsapPair to the PLC.
CPU, Rack and Slot properties are still present to preserve backwards
compatibility and to support alternate functionality based on the PLC
type.
2021-06-04 22:25:05 +02:00
Michael Croes
6465e3c8c7 Move GetDefaultTsapPair to TsapPair class 2021-06-04 22:01:40 +02:00
Michael Croes
fcd61c1236 Add CpuType.S7200Smart 2021-06-04 21:46:42 +02:00
Michael Croes
616dc1094c Refactor TSAP related code 2021-06-04 21:45:51 +02:00
Michael Croes
70bc1499ef ConnectionRequestTest: Cleanup trailing whitespace 2021-06-02 23:03:23 +02:00
Michael Croes
fd9aeb5b3b Fix unit test for S7 200 2021-06-02 23:03:06 +02:00
Michael Croes
0750ee006f Change S7 200 remote TSAP to 10.01 2021-06-02 22:06:00 +02:00
Michael Croes
5318f94dd7 Release S7NetPlus 0.12.0
Release highlights:
- Add synchronization to methods interacting with the PLC
2021-06-02 21:28:48 +02:00
Michael Croes
ea3beff481 Merge pull request #394 from mycroes/sync
Add synchronization
2021-06-02 21:25:51 +02:00
Michael Croes
f67b1e773f Add missing ConfigureAwait in OpenAsync 2021-06-01 20:59:20 +02:00
Michael Croes
e93a656312 Fix locking for OpenAsync 2021-06-01 20:28:34 +02:00
Michael Croes
8035f71a16 Apply synchronization to stream actions 2021-05-31 22:57:13 +02:00
Michael Croes
df4f258290 Move GetStreamIfAvailable to PLC.cs 2021-05-31 21:23:32 +02:00
Michael Croes
5636b93a53 PlcSynchronous: Remove unused usings 2021-05-31 21:17:42 +02:00
Michael Croes
8ed1b840bc PlcSynchronous: Clenaup trailing whitespace 2021-05-31 21:17:23 +02:00
Michael Croes
2afed88231 Consolidate sync stream calls 2021-05-31 21:17:03 +02:00
Michael Croes
1ded47971b Consolidate async stream calls 2021-05-31 21:07:16 +02:00
Michael Croes
ced10b4eca Release S7NetPlus 0.11.0
Release highlights:
- Fix for byte length calculation of bits
- Fix for Boolean.ClearBit
- Added Boolean.ClearBit and .SetBit by reference
2021-05-10 22:55:28 +02:00
Michael Croes
632e1c14ac Merge pull request #391 from mycroes/set-bit-clear-bit
SetBit and ClearBit tests, fixes
2021-05-10 21:24:43 +02:00
Michael Croes
aa50280233 Boolean: Add SetBit and ClearBit by reference 2021-05-10 21:14:43 +02:00
Michael Croes
3a794e8a46 Cleanup trailing whitespace in PLC and PLCHelpers 2021-05-10 21:05:02 +02:00
Michael Croes
0b8bd66bf7 Fix bitwise operator in Boolean.ClearBit
Fixes #249
2021-05-10 20:50:46 +02:00
Michael Croes
e66d21af05 Add tests for Boolean.SetBit and Boolean.ClearBit 2021-05-10 20:49:15 +02:00
Michael Croes
44ee651ac4 Merge pull request #390 from mycroes/byte-length-bits
Fix byte length calculation for VarType.Bit
2021-05-10 20:32:34 +02:00
Michael Croes
a1b4694ef6 Fix calculation of byte length for VarType.Bit
Fixes #389
2021-05-10 20:25:38 +02:00
Serge Camille
fff6f3458f Merge branch 'develop' into githubActions2 2021-03-16 20:22:49 +01:00
Serge Camille
ae70f31af2 Remove deploy workflow. 2020-10-17 11:47:34 +02:00
Serge Camille
fb44b56c16 Skip welcome message 2020-10-17 11:47:34 +02:00
Serge Camille
ce97fcf335 Separate restore and test steps to better see execution times.
Restore could be cached, but it looks quite complicated to pull off.
2020-10-17 11:47:34 +02:00
Serge Camille
81208c0f03 Add github actions for multi-platform unit tests. 2020-10-17 11:47:34 +02:00
21 changed files with 762 additions and 284 deletions

80
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
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-latest, macos-latest]
test-framework: [netcoreapp3.1, net5.0]
include:
- os: ubuntu-latest
test-framework: netcoreapp3.1
installSnap7: true
dotnet-sdk: '3.1.x'
- os: ubuntu-latest
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-latest' }}
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

@@ -0,0 +1,181 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace S7.Net.UnitTest
{
/// <summary>
/// Test stream which only gives 1 byte per read.
/// </summary>
class TestStreamConnectionClose : Stream
{
private readonly CancellationTokenSource _cancellationTokenSource;
public TestStreamConnectionClose(CancellationTokenSource cancellationTokenSource)
{
_cancellationTokenSource = cancellationTokenSource;
}
public override bool CanRead => false;
public override bool CanSeek => throw new NotImplementedException();
public override bool CanWrite => true;
public override long Length => throw new NotImplementedException();
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public override void Flush()
{
throw new NotImplementedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
_cancellationTokenSource.Cancel();
}
}
/// <summary>
/// These tests are intended to test <see cref="StreamExtensions"/> functions and other stream-related special cases.
/// </summary>
[TestClass]
public class ConnectionCloseTest
{
const short TestServerPort = 31122;
const string TestServerIp = "127.0.0.1";
[TestMethod]
public async Task Test_CancellationDuringTransmission()
{
var plc = new Plc(CpuType.S7300, TestServerIp, TestServerPort, 0, 2);
// Set up a shared cancellation source so we can let the stream
// initiate cancel after some data has been written to it.
var cancellationSource = new CancellationTokenSource();
var cancellationToken = cancellationSource.Token;
var stream = new TestStreamConnectionClose(cancellationSource);
var requestData = new byte[100]; // empty data, it does not matter what is in there
// Set up access to private method and field
var dynMethod = plc.GetType().GetMethod("NoLockRequestTpduAsync",
BindingFlags.NonPublic | BindingFlags.Instance);
if (dynMethod == null)
{
throw new NullReferenceException("Could not find method 'NoLockRequestTpduAsync' on Plc object.");
}
var tcpClientField = plc.GetType().GetField("tcpClient", BindingFlags.NonPublic | BindingFlags.Instance);
if (tcpClientField == null)
{
throw new NullReferenceException("Could not find field 'tcpClient' on Plc object.");
}
// Set a value to tcpClient field so we can later ensure that it has been closed.
tcpClientField.SetValue(plc, new TcpClient());
var tcpClientValue = tcpClientField.GetValue(plc);
Assert.IsNotNull(tcpClientValue);
try
{
var result = (Task<COTP.TPDU>) dynMethod.Invoke(plc, new object[] { stream, requestData, cancellationToken });
await result;
}
catch (OperationCanceledException)
{
Console.WriteLine("Task was cancelled as expected.");
// Ensure that the plc connection was closed since the task was cancelled
// after data has been sent through the network. We expect that the tcpClient
// object was set to NULL
var tcpClientValueAfter = tcpClientField.GetValue(plc);
Assert.IsNull(tcpClientValueAfter);
return;
}
catch (Exception e)
{
Assert.Fail($"Wrong exception type received. Expected {typeof(OperationCanceledException)}, received {e.GetType()}.");
}
// Ensure test fails if cancellation did not occur.
Assert.Fail("Task was not cancelled as expected.");
}
[TestMethod]
public async Task Test_CancellationBeforeTransmission()
{
var plc = new Plc(CpuType.S7300, TestServerIp, TestServerPort, 0, 2);
// Set up a cancellation source
var cancellationSource = new CancellationTokenSource();
var cancellationToken = cancellationSource.Token;
var stream = new TestStreamConnectionClose(cancellationSource);
var requestData = new byte[100]; // empty data, it does not matter what is in there
// Set up access to private method and field
var dynMethod = plc.GetType().GetMethod("NoLockRequestTpduAsync",
BindingFlags.NonPublic | BindingFlags.Instance);
if (dynMethod == null)
{
throw new NullReferenceException("Could not find method 'NoLockRequestTpduAsync' on Plc object.");
}
var tcpClientField = plc.GetType().GetField("tcpClient", BindingFlags.NonPublic | BindingFlags.Instance);
if (tcpClientField == null)
{
throw new NullReferenceException("Could not find field 'tcpClient' on Plc object.");
}
// Set a value to tcpClient field so we can later ensure that it has been closed.
tcpClientField.SetValue(plc, new TcpClient());
var tcpClientValue = tcpClientField.GetValue(plc);
Assert.IsNotNull(tcpClientValue);
try
{
// cancel the task before we start transmitting data
cancellationSource.Cancel();
var result = (Task<COTP.TPDU>)dynMethod.Invoke(plc, new object[] { stream, requestData, cancellationToken });
await result;
}
catch (OperationCanceledException)
{
Console.WriteLine("Task was cancelled as expected.");
// Ensure that the plc connection was not closed, since we cancelled the task before
// sending data through the network. We expect that the tcpClient
// object was NOT set to NULL
var tcpClientValueAfter = tcpClientField.GetValue(plc);
Assert.IsNotNull(tcpClientValueAfter);
return;
}
catch (Exception e)
{
Assert.Fail($"Wrong exception type received. Expected {typeof(OperationCanceledException)}, received {e.GetType()}.");
}
// Ensure test fails if cancellation did not occur.
Assert.Fail("Task was not cancelled as expected.");
}
}
}

View File

@@ -9,52 +9,52 @@ namespace S7.Net.UnitTest
[TestMethod]
public void Test_ConnectionRequest_S7_200()
{
CollectionAssert.AreEqual(MakeConnectionRequest(16, 0, 16, 0),
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7200, 0, 0));
CollectionAssert.AreEqual(MakeConnectionRequest(16, 0, 16, 1),
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7200, 0, 0)));
}
[TestMethod]
public void Test_ConnectionRequest_S7_300()
{
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0),
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7300, 0, 0));
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7300, 0, 0)));
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 1),
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7300, 0, 1));
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7300, 0, 1)));
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33),
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7300, 1, 1));
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7300, 1, 1)));
}
[TestMethod]
public void Test_ConnectionRequest_S7_400()
{
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0),
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7400, 0, 0));
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7400, 0, 0)));
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 1),
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7400, 0, 1));
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7400, 0, 1)));
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33),
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7400, 1, 1));
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7400, 1, 1)));
}
[TestMethod]
public void Test_ConnectionRequest_S7_1200()
{
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0),
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71200, 0, 0));
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71200, 0, 0)));
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 1),
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71200, 0, 1));
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71200, 0, 1)));
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33),
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71200, 1, 1));
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71200, 1, 1)));
}
[TestMethod]
public void Test_ConnectionRequest_S7_1500()
{
CollectionAssert.AreEqual(MakeConnectionRequest(0x10, 0x2, 3, 0),
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71500, 0, 0));
CollectionAssert.AreEqual(MakeConnectionRequest(0x10, 0x2, 3, 1),
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71500, 0, 1));
CollectionAssert.AreEqual(MakeConnectionRequest(0x10, 0x2, 3, 33),
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71500, 1, 1));
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0),
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71500, 0, 0)));
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 1),
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71500, 0, 1)));
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33),
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71500, 1, 1)));
}
private static byte[] MakeConnectionRequest(byte sourceTsap1, byte sourceTsap2, byte destTsap1, byte destTsap2)
@@ -63,7 +63,7 @@ namespace S7.Net.UnitTest
{
3, 0, 0, 22, //TPKT
17, //COTP Header Length
224, //Connect Request
224, //Connect Request
0, 0, //Destination Reference
0, 46, //Source Reference
0, //Flags

View File

@@ -1,14 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using S7.Net;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using S7.Net.Protocol;
using System.Collections;
namespace S7.Net.UnitTest
{
@@ -21,21 +17,17 @@ namespace S7.Net.UnitTest
public async Task TPKT_Read()
{
var m = new MemoryStream(StringToByteArray("0300002902f0803203000000010002001400000401ff0400807710000100000103000000033f8ccccd"));
var t = TPKT.Read(m);
Assert.AreEqual(0x03, t.Version);
Assert.AreEqual(0x29, t.Length);
m.Position = 0;
t = await TPKT.ReadAsync(m, TestContext.CancellationTokenSource.Token);
var t = await TPKT.ReadAsync(m, TestContext.CancellationTokenSource.Token);
Assert.AreEqual(0x03, t.Version);
Assert.AreEqual(0x29, t.Length);
}
[TestMethod]
[ExpectedException(typeof(TPKTInvalidException))]
public void TPKT_ReadShort()
public async Task TPKT_ReadShort()
{
var m = new MemoryStream(StringToByteArray("0300002902f0803203000000010002001400000401ff040080"));
var t = TPKT.Read(m);
var t = await TPKT.ReadAsync(m, CancellationToken.None);
}
@@ -48,14 +40,11 @@ namespace S7.Net.UnitTest
}
[TestMethod]
public void COTP_ReadTSDU()
public async Task COTP_ReadTSDU()
{
var expected = StringToByteArray("320700000400000800080001120411440100ff09000400000000");
var m = new MemoryStream(StringToByteArray("0300000702f0000300000702f0000300002102f080320700000400000800080001120411440100ff09000400000000"));
var t = COTP.TSDU.Read(m);
Assert.IsTrue(expected.SequenceEqual(t));
m.Position = 0;
t = COTP.TSDU.ReadAsync(m, TestContext.CancellationTokenSource.Token).Result;
var t = await COTP.TSDU.ReadAsync(m, TestContext.CancellationTokenSource.Token);
Assert.IsTrue(expected.SequenceEqual(t));
}
@@ -69,14 +58,13 @@ namespace S7.Net.UnitTest
[TestMethod]
public void TestResponseCode()
public async Task TestResponseCode()
{
var expected = StringToByteArray("320700000400000800080001120411440100ff09000400000000");
var m = new MemoryStream(StringToByteArray("0300000702f0000300000702f0000300002102f080320700000400000800080001120411440100ff09000400000000"));
var t = COTP.TSDU.Read(m);
var t = await COTP.TSDU.ReadAsync(m, CancellationToken.None);
Assert.IsTrue(expected.SequenceEqual(t));
// Test all possible byte values. Everything except 0xff should throw an exception.
var testData = Enumerable.Range(0, 256).Select(i => new { StatusCode = (ReadWriteErrorCode)i, ThrowsException = i != (byte)ReadWriteErrorCode.Success });

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net452;netcoreapp3.1</TargetFrameworks>
<TargetFrameworks>net452;netcoreapp3.1;net5.0</TargetFrameworks>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile>

View File

@@ -935,20 +935,53 @@ namespace S7.Net.UnitTest
{
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data, cancellationToken);
}
catch(TaskCanceledException)
catch(OperationCanceledException)
{
// everything is good, that is the exception we expect
Console.WriteLine("Task was cancelled as expected.");
Console.WriteLine("Operation was cancelled as expected.");
return;
}
catch(Exception e)
{
Assert.Fail($"Wrong exception type received. Expected {typeof(TaskCanceledException)}, received {e.GetType()}.");
Assert.Fail($"Wrong exception type received. Expected {typeof(OperationCanceledException)}, received {e.GetType()}.");
}
// Depending on how tests run, this can also just succeed without getting cancelled at all. Do nothing in this case.
Console.WriteLine("Task was not cancelled as expected.");
}
/// <summary>
/// Write a large amount of data and test cancellation
/// </summary>
[TestMethod]
public async Task Test_Async_ParseDataIntoDataItemsAlignment()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
var db = 2;
// First write a sensible S7 string capacity
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, new byte[] {5, 0});
// Read two data items, with the first having odd number of bytes (7),
// and the second has to be aligned on a even address
var dataItems = new List<DataItem>
{
new DataItem
{
DataType = DataType.DataBlock,
DB = db,
VarType = VarType.S7String,
Count = 5
},
new DataItem
{
DataType = DataType.DataBlock,
DB = db,
VarType = VarType.Word,
}
};
await plc.ReadMultipleVarsAsync(dataItems, CancellationToken.None);
}
#endregion
}
}

View File

@@ -2,6 +2,7 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace S7.Net.UnitTest
@@ -15,6 +16,7 @@ namespace S7.Net.UnitTest
{
Data = data;
}
public override bool CanRead => _position < Data.Length;
public override bool CanSeek => throw new NotImplementedException();
@@ -26,21 +28,31 @@ namespace S7.Net.UnitTest
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public byte[] Data { get; }
int _position = 0;
public override void Flush()
{
throw new NotImplementedException();
}
int _position = 0;
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (_position >= Data.Length)
{
return 0;
return Task.FromResult(0);
}
buffer[offset] = Data[_position];
++_position;
return 1;
return Task.FromResult(1);
}
public override long Seek(long offset, SeekOrigin origin)
@@ -78,21 +90,21 @@ namespace S7.Net.UnitTest
}
[TestMethod]
public void TPKT_ReadRestrictedStream()
public async Task TPKT_ReadRestrictedStream()
{
var fullMessage = ProtocolUnitTest.StringToByteArray("0300002902f0803203000000010002001400000401ff0400807710000100000103000000033f8ccccd");
var m = new TestStream1BytePerRead(fullMessage);
var t = TPKT.Read(m);
var t = await TPKT.ReadAsync(m, CancellationToken.None);
Assert.AreEqual(fullMessage.Length, t.Length);
Assert.AreEqual(fullMessage.Last(), t.Data.Last());
}
[TestMethod]
public void TPKT_ReadStreamTooShort()
public async Task TPKT_ReadStreamTooShort()
{
var fullMessage = ProtocolUnitTest.StringToByteArray("0300002902f0803203000000010002001400");
var m = new TestStream1BytePerRead(fullMessage);
Assert.ThrowsException<TPKTInvalidException>(() => TPKT.Read(m));
await Assert.ThrowsExceptionAsync<TPKTInvalidException>(() => TPKT.ReadAsync(m, CancellationToken.None));
}
}
}

View File

@@ -0,0 +1,38 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Boolean = S7.Net.Types.Boolean;
namespace S7.Net.UnitTest.TypeTests
{
[TestClass]
public class BooleanTests
{
[DataTestMethod]
[DataRow(0)]
[DataRow(1)]
[DataRow(2)]
[DataRow(3)]
[DataRow(4)]
[DataRow(5)]
[DataRow(6)]
[DataRow(7)]
public void TestValidSetBitValues(int index)
{
Assert.AreEqual(Math.Pow(2, index), Boolean.SetBit(0, index));
}
[DataTestMethod]
[DataRow(0)]
[DataRow(1)]
[DataRow(2)]
[DataRow(3)]
[DataRow(4)]
[DataRow(5)]
[DataRow(6)]
[DataRow(7)]
public void TestValidClearBitValues(int index)
{
Assert.AreEqual((byte) ((uint) Math.Pow(2, index) ^ uint.MaxValue), Boolean.ClearBit(byte.MaxValue, index));
}
}
}

View File

@@ -50,22 +50,6 @@ namespace S7.Net
Data = new byte[0];
}
/// <summary>
/// Reads COTP TPDU (Transport protocol data unit) from the network stream
/// See: https://tools.ietf.org/html/rfc905
/// </summary>
/// <param name="stream">The socket to read from</param>
/// <returns>COTP DPDU instance</returns>
public static TPDU Read(Stream stream)
{
var tpkt = TPKT.Read(stream);
if (tpkt.Length == 0)
{
throw new TPDUInvalidException("No protocol data received");
}
return new TPDU(tpkt);
}
/// <summary>
/// Reads COTP TPDU (Transport protocol data unit) from the network stream
/// See: https://tools.ietf.org/html/rfc905
@@ -100,36 +84,6 @@ namespace S7.Net
/// </summary>
public class TSDU
{
/// <summary>
/// Reads the full COTP TSDU (Transport service data unit)
/// See: https://tools.ietf.org/html/rfc905
/// </summary>
/// <param name="stream">The stream to read from</param>
/// <returns>Data in TSDU</returns>
public static byte[] Read(Stream stream)
{
var segment = TPDU.Read(stream);
if (segment.LastDataUnit)
{
return segment.Data;
}
// More segments are expected, prepare a buffer to store all data
var buffer = new byte[segment.Data.Length];
Array.Copy(segment.Data, buffer, segment.Data.Length);
while (!segment.LastDataUnit)
{
segment = TPDU.Read(stream);
var previousLength = buffer.Length;
Array.Resize(ref buffer, buffer.Length + segment.Data.Length);
Array.Copy(segment.Data, 0, buffer, previousLength, segment.Data.Length);
}
return buffer;
}
/// <summary>
/// Reads the full COTP TSDU (Transport service data unit)
/// See: https://tools.ietf.org/html/rfc905
@@ -137,7 +91,7 @@ namespace S7.Net
/// <param name="stream">The stream to read from</param>
/// <returns>Data in TSDU</returns>
public static async Task<byte[]> ReadAsync(Stream stream, CancellationToken cancellationToken)
{
{
var segment = await TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
if (segment.LastDataUnit)

View File

@@ -15,6 +15,11 @@
/// </summary>
Logo0BA8 = 1,
/// <summary>
/// S7 200 Smart
/// </summary>
S7200Smart = 2,
/// <summary>
/// S7 300 cpu type
/// </summary>

View File

@@ -0,0 +1,28 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace S7.Net.Internal
{
internal class TaskQueue
{
private static readonly object Sentinel = new object();
private Task prev = Task.FromResult(Sentinel);
public async Task<T> Enqueue<T>(Func<Task<T>> action)
{
var tcs = new TaskCompletionSource<object>();
await Interlocked.Exchange(ref prev, tcs.Task).ConfigureAwait(false);
try
{
return await action.Invoke().ConfigureAwait(false);
}
finally
{
tcs.SetResult(Sentinel);
}
}
}
}

View File

@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using S7.Net.Internal;
using S7.Net.Protocol;
using S7.Net.Types;
@@ -13,14 +15,24 @@ namespace S7.Net
/// </summary>
public partial class Plc : IDisposable
{
private const int CONNECTION_TIMED_OUT_ERROR_CODE = 10060;
/// <summary>
/// The default port for the S7 protocol.
/// </summary>
public const int DefaultPort = 102;
/// <summary>
/// The default timeout (in milliseconds) used for <see cref="P:ReadTimeout"/> and <see cref="P:WriteTimeout"/>.
/// </summary>
public const int DefaultTimeout = 10_000;
private readonly TaskQueue queue = new TaskQueue();
//TCP connection to device
private TcpClient? tcpClient;
private NetworkStream? _stream;
private int readTimeout = 0; // default no timeout
private int writeTimeout = 0; // default no timeout
private int readTimeout = DefaultTimeout; // default no timeout
private int writeTimeout = DefaultTimeout; // default no timeout
/// <summary>
/// IP address of the PLC
@@ -32,6 +44,11 @@ namespace S7.Net
/// </summary>
public int Port { get; }
/// <summary>
/// The TSAP addresses used during the connection request.
/// </summary>
public TsapPair TsapPair { get; set; }
/// <summary>
/// CPU type of the PLC
/// </summary>
@@ -104,25 +121,14 @@ namespace S7.Net
/// </summary>
/// <param name="cpu">CpuType of the PLC (select from the enum)</param>
/// <param name="ip">Ip address of the PLC</param>
/// <param name="port">Port address of the PLC, default 102</param>
/// <param name="rack">rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal</param>
/// <param name="slot">slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500.
/// If you use an external ethernet card, this must be set accordingly.</param>
public Plc(CpuType cpu, string ip, int port, Int16 rack, Int16 slot)
public Plc(CpuType cpu, string ip, Int16 rack, Int16 slot)
: this(cpu, ip, DefaultPort, rack, slot)
{
if (!Enum.IsDefined(typeof(CpuType), cpu))
throw new ArgumentException($"The value of argument '{nameof(cpu)}' ({cpu}) is invalid for Enum type '{typeof(CpuType).Name}'.", nameof(cpu));
if (string.IsNullOrEmpty(ip))
throw new ArgumentException("IP address must valid.", nameof(ip));
CPU = cpu;
IP = ip;
Port = port;
Rack = rack;
Slot = slot;
MaxPDUSize = 240;
}
/// <summary>
/// Creates a PLC object with all the parameters needed for connections.
/// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0.
@@ -131,23 +137,51 @@ namespace S7.Net
/// </summary>
/// <param name="cpu">CpuType of the PLC (select from the enum)</param>
/// <param name="ip">Ip address of the PLC</param>
/// <param name="port">Port number used for the connection, default 102.</param>
/// <param name="rack">rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal</param>
/// <param name="slot">slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500.
/// If you use an external ethernet card, this must be set accordingly.</param>
public Plc(CpuType cpu, string ip, Int16 rack, Int16 slot)
public Plc(CpuType cpu, string ip, int port, Int16 rack, Int16 slot)
: this(ip, port, TsapPair.GetDefaultTsapPair(cpu, rack, slot))
{
if (!Enum.IsDefined(typeof(CpuType), cpu))
throw new ArgumentException($"The value of argument '{nameof(cpu)}' ({cpu}) is invalid for Enum type '{typeof(CpuType).Name}'.", nameof(cpu));
throw new ArgumentException(
$"The value of argument '{nameof(cpu)}' ({cpu}) is invalid for Enum type '{typeof(CpuType).Name}'.",
nameof(cpu));
CPU = cpu;
Rack = rack;
Slot = slot;
}
/// <summary>
/// Creates a PLC object with all the parameters needed for connections.
/// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0.
/// You need slot > 0 if you are connecting to external ethernet card (CP).
/// For S7-300 and S7-400 the default is rack = 0 and slot = 2.
/// </summary>
/// <param name="ip">Ip address of the PLC</param>
/// <param name="tsapPair">The TSAP addresses used for the connection request.</param>
public Plc(string ip, TsapPair tsapPair) : this(ip, DefaultPort, tsapPair)
{
}
/// <summary>
/// Creates a PLC object with all the parameters needed for connections. Use this constructor
/// if you want to manually override the TSAP addresses used during the connection request.
/// </summary>
/// <param name="ip">Ip address of the PLC</param>
/// <param name="port">Port number used for the connection, default 102.</param>
/// <param name="tsapPair">The TSAP addresses used for the connection request.</param>
public Plc(string ip, int port, TsapPair tsapPair)
{
if (string.IsNullOrEmpty(ip))
throw new ArgumentException("IP address must valid.", nameof(ip));
CPU = cpu;
IP = ip;
Port = 102;
Rack = rack;
Slot = slot;
Port = port;
MaxPDUSize = 240;
TsapPair = tsapPair;
}
/// <summary>
@@ -158,6 +192,7 @@ namespace S7.Net
if (tcpClient != null)
{
if (tcpClient.Connected) tcpClient.Close();
tcpClient = null; // Can not reuse TcpClient once connection gets closed.
}
}
@@ -242,6 +277,16 @@ namespace S7.Net
}
}
private Stream GetStreamIfAvailable()
{
if (_stream == null)
{
throw new PlcException(ErrorCode.ConnectionError, "Plc is not connected");
}
return _stream;
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls

View File

@@ -30,8 +30,8 @@ namespace S7.Net
}
/// <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.
/// 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="dataType">MemoryType (DB, Timer, Counter, etc.)</param>
/// <param name="db">Address of the memory to be read</param>
@@ -184,7 +184,7 @@ namespace S7.Net
switch (varType)
{
case VarType.Bit:
return varCount + 7 / 8;
return (varCount + 7) / 8;
case VarType.Byte:
return (varCount < 1) ? 1 : varCount;
case VarType.String:
@@ -242,8 +242,8 @@ namespace S7.Net
// next Item
offset += byteCnt;
// Fill byte in response when bytecount is odd
if (dataItem.Count % 2 != 0 && (dataItem.VarType == VarType.Byte || dataItem.VarType == VarType.Bit))
// Always align to even offset
if (offset % 2 != 0)
offset++;
}
}

View File

@@ -1,11 +1,11 @@
using S7.Net.Types;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Threading.Tasks;
using S7.Net.Protocol;
using System.IO;
using System.Threading;
using S7.Net.Protocol.S7;
@@ -28,11 +28,16 @@ namespace S7.Net
var stream = await ConnectAsync().ConfigureAwait(false);
try
{
cancellationToken.ThrowIfCancellationRequested();
await EstablishConnection(stream, cancellationToken).ConfigureAwait(false);
_stream = stream;
await queue.Enqueue(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
await EstablishConnection(stream, cancellationToken).ConfigureAwait(false);
_stream = stream;
return default(object);
}).ConfigureAwait(false);
}
catch(Exception)
catch (Exception)
{
stream.Dispose();
throw;
@@ -47,29 +52,30 @@ namespace S7.Net
return tcpClient.GetStream();
}
private async Task EstablishConnection(NetworkStream stream, CancellationToken cancellationToken)
private async Task EstablishConnection(Stream stream, CancellationToken cancellationToken)
{
await RequestConnection(stream, cancellationToken).ConfigureAwait(false);
await SetupConnection(stream, cancellationToken).ConfigureAwait(false);
}
private async Task RequestConnection(NetworkStream stream, CancellationToken cancellationToken)
private async Task RequestConnection(Stream stream, CancellationToken cancellationToken)
{
var requestData = ConnectionRequest.GetCOTPConnectionRequest(CPU, Rack, Slot);
await stream.WriteAsync(requestData, 0, requestData.Length).ConfigureAwait(false);
var response = await COTP.TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
var requestData = ConnectionRequest.GetCOTPConnectionRequest(TsapPair);
var response = await NoLockRequestTpduAsync(stream, requestData, cancellationToken).ConfigureAwait(false);
if (response.PDUType != COTP.PduType.ConnectionConfirmed)
{
throw new InvalidDataException("Connection request was denied", response.TPkt.Data, 1, 0x0d);
}
}
private async Task SetupConnection(NetworkStream stream, CancellationToken cancellationToken)
private async Task SetupConnection(Stream stream, CancellationToken cancellationToken)
{
var setupData = GetS7ConnectionSetup();
await stream.WriteAsync(setupData, 0, setupData.Length).ConfigureAwait(false);
var s7data = await COTP.TSDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
var s7data = await NoLockRequestTsduAsync(stream, setupData, 0, setupData.Length, cancellationToken)
.ConfigureAwait(false);
if (s7data.Length < 2)
throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup");
@@ -112,7 +118,7 @@ namespace S7.Net
}
/// <summary>
/// Read and decode a certain number of bytes of the "VarType" provided.
/// Read and decode a certain number of bytes of the "VarType" provided.
/// This can be used to read multiple consecutive variables of the same type (Word, DWord, Int, etc).
/// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary>
@@ -179,10 +185,10 @@ namespace S7.Net
}
/// <summary>
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
/// </summary>
/// <param name="sourceClass">Instance of the class that will store the values</param>
/// <param name="sourceClass">Instance of the class that will store the values</param>
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
@@ -205,7 +211,7 @@ namespace S7.Net
}
/// <summary>
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified. To instantiate the class defined by the generic
/// type, the class needs a default constructor.
/// </summary>
@@ -221,7 +227,7 @@ namespace S7.Net
}
/// <summary>
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
/// </summary>
/// <typeparam name="T">The class that will be instantiated</typeparam>
@@ -245,10 +251,10 @@ namespace S7.Net
}
/// <summary>
/// Reads multiple vars in a single request.
/// Reads multiple vars in a single request.
/// You have to create and pass a list of DataItems and you obtain in response the same list with the values.
/// Values are stored in the property "Value" of the dataItem and are already converted.
/// If you don't want the conversion, just create a dataItem of bytes.
/// If you don't want the conversion, just create a dataItem of bytes.
/// The number of DataItems as well as the total size of the requested data can not exceed a certain limit (protocol restriction).
/// </summary>
/// <param name="dataItems">List of dataitems that contains the list of variables that must be read.</param>
@@ -256,18 +262,15 @@ namespace S7.Net
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
public async Task<List<DataItem>> ReadMultipleVarsAsync(List<DataItem> dataItems, CancellationToken cancellationToken = default)
{
//Snap7 seems to choke on PDU sizes above 256 even if snap7
//Snap7 seems to choke on PDU sizes above 256 even if snap7
//replies with bigger PDU size in connection setup.
AssertPduSizeForRead(dataItems);
var stream = GetStreamIfAvailable();
try
{
var dataToSend = BuildReadRequestPackage(dataItems.Select(d => DataItem.GetDataItemAddress(d)).ToList());
await stream.WriteAsync(dataToSend, 0, dataToSend.Length).ConfigureAwait(false);
var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
var s7data = await COTP.TSDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
ParseDataIntoDataItems(s7data, dataItems);
@@ -435,12 +438,9 @@ namespace S7.Net
private async Task ReadBytesWithSingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
var stream = GetStreamIfAvailable();
var dataToSend = BuildReadRequestPackage(new[] { new DataItemAddress(dataType, db, startByteAdr, count) });
var dataToSend = BuildReadRequestPackage(new [] { new DataItemAddress(dataType, db, startByteAdr, count)});
await stream.WriteAsync(dataToSend, 0, dataToSend.Length, cancellationToken);
var s7data = await COTP.TSDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
AssertReadResponse(s7data, count);
Array.Copy(s7data, 18, buffer, offset, count);
@@ -456,13 +456,11 @@ namespace S7.Net
{
AssertPduSizeForWrite(dataItems);
var stream = GetStreamIfAvailable();
var message = new ByteArray();
var length = S7WriteMultiple.CreateRequest(message, dataItems);
await stream.WriteAsync(message.Array, 0, length).ConfigureAwait(false);
var response = await COTP.TSDU.ReadAsync(stream, CancellationToken.None).ConfigureAwait(false);
var response = await RequestTsduAsync(message.Array, 0, length).ConfigureAwait(false);
S7WriteMultiple.ParseResponse(response, response.Length, dataItems);
}
@@ -476,15 +474,11 @@ namespace S7.Net
/// <returns>A task that represents the asynchronous write operation.</returns>
private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count, CancellationToken cancellationToken)
{
try
{
var stream = GetStreamIfAvailable();
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value, dataOffset, count);
var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false);
await stream.WriteAsync(dataToSend, 0, dataToSend.Length, cancellationToken).ConfigureAwait(false);
var s7data = await COTP.TSDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (OperationCanceledException)
@@ -499,15 +493,11 @@ namespace S7.Net
private async Task WriteBitWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue, CancellationToken cancellationToken)
{
var stream = GetStreamIfAvailable();
try
{
var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr);
var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false);
await stream.WriteAsync(dataToSend, 0, dataToSend.Length).ConfigureAwait(false);
var s7data = await COTP.TSDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (OperationCanceledException)
@@ -520,13 +510,57 @@ namespace S7.Net
}
}
private Stream GetStreamIfAvailable()
private Task<byte[]> RequestTsduAsync(byte[] requestData, CancellationToken cancellationToken = default) =>
RequestTsduAsync(requestData, 0, requestData.Length, cancellationToken);
private Task<byte[]> RequestTsduAsync(byte[] requestData, int offset, int length, CancellationToken cancellationToken = default)
{
if (_stream == null)
var stream = GetStreamIfAvailable();
return queue.Enqueue(() =>
NoLockRequestTsduAsync(stream, requestData, offset, length, cancellationToken));
}
private async Task<COTP.TPDU> NoLockRequestTpduAsync(Stream stream, byte[] requestData,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
throw new PlcException(ErrorCode.ConnectionError, "Plc is not connected");
using var closeOnCancellation = cancellationToken.Register(Close);
await stream.WriteAsync(requestData, 0, requestData.Length, cancellationToken).ConfigureAwait(false);
return await COTP.TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
}
catch (Exception exc)
{
if (exc is TPDUInvalidException || exc is TPKTInvalidException)
{
Close();
}
throw;
}
}
private async Task<byte[]> NoLockRequestTsduAsync(Stream stream, byte[] requestData, int offset, int length,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
using var closeOnCancellation = cancellationToken.Register(Close);
await stream.WriteAsync(requestData, offset, length, cancellationToken).ConfigureAwait(false);
return await COTP.TSDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
}
catch (Exception exc)
{
if (exc is TPDUInvalidException || exc is TPKTInvalidException)
{
Close();
}
throw;
}
return _stream;
}
}
}

View File

@@ -2,8 +2,6 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using S7.Net.Protocol;
using S7.Net.Helper;
@@ -54,7 +52,7 @@ namespace S7.Net
}
/// <summary>
/// Read and decode a certain number of bytes of the "VarType" provided.
/// Read and decode a certain number of bytes of the "VarType" provided.
/// This can be used to read multiple consecutive variables of the same type (Word, DWord, Int, etc).
/// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary>
@@ -115,10 +113,10 @@ namespace S7.Net
/// <summary>
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
/// </summary>
/// <param name="sourceClass">Instance of the class that will store the values</param>
/// <param name="sourceClass">Instance of the class that will store the values</param>
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>The number of read bytes</returns>
@@ -138,7 +136,7 @@ namespace S7.Net
}
/// <summary>
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified. To instantiate the class defined by the generic
/// type, the class needs a default constructor.
/// </summary>
@@ -152,7 +150,7 @@ namespace S7.Net
}
/// <summary>
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
/// </summary>
/// <typeparam name="T">The class that will be instantiated</typeparam>
@@ -186,7 +184,7 @@ namespace S7.Net
while (count > 0)
{
//TODO: Figure out how to use MaxPDUSize here
//Snap7 seems to choke on PDU sizes above 256 even if snap7
//Snap7 seems to choke on PDU sizes above 256 even if snap7
//replies with bigger PDU size in connection setup.
var maxToWrite = Math.Min(count, MaxPDUSize - 28);//TODO tested only when the MaxPDUSize is 480
WriteBytesWithASingleRequest(dataType, db, startByteAdr + localIndex, value, localIndex, maxToWrite);
@@ -298,7 +296,6 @@ namespace S7.Net
private void ReadBytesWithSingleRequest(DataType dataType, int db, int startByteAdr, byte[] buffer, int offset, int count)
{
var stream = GetStreamIfAvailable();
try
{
// first create the header
@@ -309,9 +306,7 @@ namespace S7.Net
BuildReadDataRequestPackage(package, dataType, db, startByteAdr, count);
var dataToSend = package.ToArray();
stream.Write(dataToSend, 0, dataToSend.Length);
var s7data = COTP.TSDU.Read(stream);
var s7data = RequestTsdu(dataToSend);
AssertReadResponse(s7data, count);
Array.Copy(s7data, 18, buffer, offset, count);
@@ -331,13 +326,11 @@ namespace S7.Net
{
AssertPduSizeForWrite(dataItems);
var stream = GetStreamIfAvailable();
var message = new ByteArray();
var length = S7WriteMultiple.CreateRequest(message, dataItems);
stream.Write(message.Array, 0, length);
var response = RequestTsdu(message.Array, 0, length);
var response = COTP.TSDU.Read(stream);
S7WriteMultiple.ParseResponse(response, response.Length, dataItems);
}
@@ -345,12 +338,9 @@ namespace S7.Net
{
try
{
var stream = GetStreamIfAvailable();
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value, dataOffset, count);
var s7data = RequestTsdu(dataToSend);
stream.Write(dataToSend, 0, dataToSend.Length);
var s7data = COTP.TSDU.Read(stream);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (Exception exc)
@@ -425,14 +415,11 @@ namespace S7.Net
private void WriteBitWithASingleRequest(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue)
{
var stream = GetStreamIfAvailable();
try
{
var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr);
var s7data = RequestTsdu(dataToSend);
stream.Write(dataToSend, 0, dataToSend.Length);
var s7data = COTP.TSDU.Read(stream);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (Exception exc)
@@ -442,10 +429,10 @@ namespace S7.Net
}
/// <summary>
/// Reads multiple vars in a single request.
/// Reads multiple vars in a single request.
/// You have to create and pass a list of DataItems and you obtain in response the same list with the values.
/// Values are stored in the property "Value" of the dataItem and are already converted.
/// If you don't want the conversion, just create a dataItem of bytes.
/// If you don't want the conversion, just create a dataItem of bytes.
/// The number of DataItems as well as the total size of the requested data can not exceed a certain limit (protocol restriction).
/// </summary>
/// <param name="dataItems">List of dataitems that contains the list of variables that must be read.</param>
@@ -453,8 +440,6 @@ namespace S7.Net
{
AssertPduSizeForRead(dataItems);
var stream = GetStreamIfAvailable();
try
{
// first create the header
@@ -468,9 +453,7 @@ namespace S7.Net
}
var dataToSend = package.ToArray();
stream.Write(dataToSend, 0, dataToSend.Length);
var s7data = COTP.TSDU.Read(stream); //TODO use Async
var s7data = RequestTsdu(dataToSend);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
@@ -481,5 +464,12 @@ namespace S7.Net
throw new PlcException(ErrorCode.ReadData, exc);
}
}
private byte[] RequestTsdu(byte[] requestData) => RequestTsdu(requestData, 0, requestData.Length);
private byte[] RequestTsdu(byte[] requestData, int offset, int length)
{
return RequestTsduAsync(requestData, offset, length).GetAwaiter().GetResult();
}
}
}

View File

@@ -1,68 +1,27 @@
using System;
namespace S7.Net.Protocol
namespace S7.Net.Protocol
{
internal static class ConnectionRequest
{
public static byte[] GetCOTPConnectionRequest(CpuType cpu, Int16 rack, Int16 slot)
public static byte[] GetCOTPConnectionRequest(TsapPair tsapPair)
{
byte[] bSend1 = {
3, 0, 0, 22, //TPKT
17, //COTP Header Length
224, //Connect Request
224, //Connect Request
0, 0, //Destination Reference
0, 46, //Source Reference
0, //Flags
193, //Parameter Code (src-tasp)
2, //Parameter Length
1, 0, //Source TASP
tsapPair.Local.FirstByte, tsapPair.Local.SecondByte, //Source TASP
194, //Parameter Code (dst-tasp)
2, //Parameter Length
3, 0, //Destination TASP
tsapPair.Remote.FirstByte, tsapPair.Remote.SecondByte, //Destination TASP
192, //Parameter Code (tpdu-size)
1, //Parameter Length
10 //TPDU Size (2^10 = 1024)
};
switch (cpu)
{
case CpuType.S7200:
//S7200: Chr(193) & Chr(2) & Chr(16) & Chr(0) 'Eigener Tsap
bSend1[13] = 0x10;
bSend1[14] = 0x00;
//S7200: Chr(194) & Chr(2) & Chr(16) & Chr(0) 'Fremder Tsap
bSend1[17] = 0x10;
bSend1[18] = 0x00;
break;
case CpuType.Logo0BA8:
// These values are taken from NodeS7, it's not verified if these are
// exact requirements to connect to the Logo0BA8.
bSend1[13] = 0x01;
bSend1[14] = 0x00;
bSend1[17] = 0x01;
bSend1[18] = 0x02;
break;
case CpuType.S71200:
case CpuType.S7300:
case CpuType.S7400:
//S7300: Chr(193) & Chr(2) & Chr(1) & Chr(0) 'Eigener Tsap
bSend1[13] = 0x01;
bSend1[14] = 0x00;
//S7300: Chr(194) & Chr(2) & Chr(3) & Chr(2) 'Fremder Tsap
bSend1[17] = 0x03;
bSend1[18] = (byte) ((rack << 5) | (int) slot);
break;
case CpuType.S71500:
// Eigener Tsap
bSend1[13] = 0x10;
bSend1[14] = 0x02;
// Fredmer Tsap
bSend1[17] = 0x03;
bSend1[18] = (byte) ((rack << 5) | (int) slot);
break;
default:
throw new Exception("Wrong CPU Type Secified");
}
return bSend1;
}
}

31
S7.Net/Protocol/Tsap.cs Normal file
View File

@@ -0,0 +1,31 @@
namespace S7.Net.Protocol
{
/// <summary>
/// Provides a representation of the Transport Service Access Point, or TSAP in short. TSAP's are used
/// to specify a client and server address. For most PLC types a default TSAP is available that allows
/// connection from any IP and can be calculated using the rack and slot numbers.
/// </summary>
public struct Tsap
{
/// <summary>
/// First byte of the TSAP.
/// </summary>
public byte FirstByte { get; set; }
/// <summary>
/// Second byte of the TSAP.
/// </summary>
public byte SecondByte { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="Tsap" /> class using the specified values.
/// </summary>
/// <param name="firstByte">The first byte of the TSAP.</param>
/// <param name="secondByte">The second byte of the TSAP.</param>
public Tsap(byte firstByte, byte secondByte)
{
FirstByte = firstByte;
SecondByte = secondByte;
}
}
}

View File

@@ -0,0 +1,96 @@
using System;
namespace S7.Net.Protocol
{
/// <summary>
/// Implements a pair of TSAP addresses used to connect to a PLC.
/// </summary>
public class TsapPair
{
/// <summary>
/// The local <see cref="Tsap" />.
/// </summary>
public Tsap Local { get; set; }
/// <summary>
/// The remote <see cref="Tsap" />
/// </summary>
public Tsap Remote { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="TsapPair" /> class using the specified local and
/// remote TSAP.
/// </summary>
/// <param name="local">The local TSAP.</param>
/// <param name="remote">The remote TSAP.</param>
public TsapPair(Tsap local, Tsap remote)
{
Local = local;
Remote = remote;
}
/// <summary>
/// Builds a <see cref="TsapPair" /> that can be used to connect to a PLC using the default connection
/// addresses.
/// </summary>
/// <remarks>
/// The remote TSAP is constructed using <code>new Tsap(0x03, (byte) ((rack &lt;&lt; 5) | slot))</code>.
/// </remarks>
/// <param name="cpuType">The CPU type of the PLC.</param>
/// <param name="rack">The rack of the PLC's network card.</param>
/// <param name="slot">The slot of the PLC's network card.</param>
/// <returns>A TSAP pair that matches the given parameters.</returns>
/// <exception cref="ArgumentOutOfRangeException">The <paramref name="cpuType"/> is invalid.
///
/// -or-
///
/// The <paramref name="rack"/> parameter is less than 0.
///
/// -or-
///
/// The <paramref name="rack"/> parameter is greater than 15.
///
/// -or-
///
/// The <paramref name="slot"/> parameter is less than 0.
///
/// -or-
///
/// The <paramref name="slot"/> parameter is greater than 15.</exception>
public static TsapPair GetDefaultTsapPair(CpuType cpuType, int rack, int slot)
{
if (rack < 0) throw InvalidRackOrSlot(rack, nameof(rack), "minimum", 0);
if (rack > 0x0F) throw InvalidRackOrSlot(rack, nameof(rack), "maximum", 0x0F);
if (slot < 0) throw InvalidRackOrSlot(slot, nameof(slot), "minimum", 0);
if (slot > 0x0F) throw InvalidRackOrSlot(slot, nameof(slot), "maximum", 0x0F);
switch (cpuType)
{
case CpuType.S7200:
return new TsapPair(new Tsap(0x10, 0x00), new Tsap(0x10, 0x01));
case CpuType.Logo0BA8:
// The actual values are probably on a per-project basis
return new TsapPair(new Tsap(0x01, 0x00), new Tsap(0x01, 0x02));
case CpuType.S7200Smart:
case CpuType.S71200:
case CpuType.S71500:
case CpuType.S7300:
case CpuType.S7400:
// Testing with S7 1500 shows only the remote TSAP needs to match. This might differ for other
// PLC types.
return new TsapPair(new Tsap(0x01, 0x00), new Tsap(0x03, (byte) ((rack << 5) | slot)));
default:
throw new ArgumentOutOfRangeException(nameof(cpuType), "Invalid CPU Type specified");
}
}
private static ArgumentOutOfRangeException InvalidRackOrSlot(int value, string name, string extrema,
int extremaValue)
{
return new ArgumentOutOfRangeException(name,
$"Invalid {name} value specified (decimal: {value}, hexadecimal: {value:X}), {extrema} value " +
$"is {extremaValue} (decimal) or {extremaValue:X} (hexadecimal).");
}
}
}

View File

@@ -25,34 +25,6 @@ namespace S7.Net
Data = data;
}
/// <summary>
/// Reads a TPKT from the socket
/// </summary>
/// <param name="stream">The stream to read from</param>
/// <returns>TPKT Instance</returns>
public static TPKT Read(Stream stream)
{
var buf = new byte[4];
int len = stream.ReadExact(buf, 0, 4);
if (len < 4) throw new TPKTInvalidException($"TPKT header is incomplete / invalid. Received Bytes: {len} expected: {buf.Length}");
var version = buf[0];
var reserved1 = buf[1];
var length = buf[2] * 256 + buf[3]; //BigEndian
var data = new byte[length - 4];
len = stream.ReadExact(data, 0, data.Length);
if (len < data.Length)
throw new TPKTInvalidException($"TPKT payload is incomplete / invalid. Received Bytes: {len} expected: {data.Length}");
return new TPKT
(
version: version,
reserved1: reserved1,
length: length,
data: data
);
}
/// <summary>
/// Reads a TPKT from the socket Async
/// </summary>

View File

@@ -14,20 +14,51 @@
}
/// <summary>
/// Sets the value of a bit to 1 (true), given the address of the bit
/// Sets the value of a bit to 1 (true), given the address of the bit. Returns
/// a copy of the value with the bit set.
/// </summary>
/// <param name="value">The input value to modify.</param>
/// <param name="bit">The index (zero based) of the bit to set.</param>
/// <returns>The modified value with the bit at index set.</returns>
public static byte SetBit(byte value, int bit)
{
return (byte)((value | (1 << bit)) & 0xFF);
SetBit(ref value, bit);
return value;
}
/// <summary>
/// Sets the value of a bit to 1 (true), given the address of the bit.
/// </summary>
/// <param name="value">The value to modify.</param>
/// <param name="bit">The index (zero based) of the bit to set.</param>
public static void SetBit(ref byte value, int bit)
{
value = (byte) ((value | (1 << bit)) & 0xFF);
}
/// <summary>
/// Resets the value of a bit to 0 (false), given the address of the bit. Returns
/// a copy of the value with the bit cleared.
/// </summary>
/// <param name="value">The input value to modify.</param>
/// <param name="bit">The index (zero based) of the bit to clear.</param>
/// <returns>The modified value with the bit at index cleared.</returns>
public static byte ClearBit(byte value, int bit)
{
ClearBit(ref value, bit);
return value;
}
/// <summary>
/// Resets the value of a bit to 0 (false), given the address of the bit
/// </summary>
public static byte ClearBit(byte value, int bit)
/// <param name="value">The input value to modify.</param>
/// <param name="bit">The index (zero based) of the bit to clear.</param>
public static void ClearBit(ref byte value, int bit)
{
return (byte)((value | (~(1 << bit))) & 0xFF);
value = (byte) (value & ~(1 << bit) & 0xFF);
}
}
}

1
S7.sln
View File

@@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
appveyor.yml = appveyor.yml
README.md = README.md
.github\workflows\test.yml = .github\workflows\test.yml
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "S7.Net.UnitTest", "S7.Net.UnitTest\S7.Net.UnitTest.csproj", "{303CCED6-9ABC-4899-A509-743341AAA804}"