diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 00000000..0d138107
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -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 }}
diff --git a/S7.Net.UnitTest/ConnectionCloseTest.cs b/S7.Net.UnitTest/ConnectionCloseTest.cs
new file mode 100644
index 00000000..87c9ea99
--- /dev/null
+++ b/S7.Net.UnitTest/ConnectionCloseTest.cs
@@ -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
+{
+ ///
+ /// Test stream which only gives 1 byte per read.
+ ///
+ 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();
+ }
+ }
+
+ ///
+ /// These tests are intended to test functions and other stream-related special cases.
+ ///
+ [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) 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)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.");
+ }
+ }
+}
diff --git a/S7.Net.UnitTest/ConnectionRequestTest.cs b/S7.Net.UnitTest/ConnectionRequestTest.cs
index 440a962f..ec171252 100644
--- a/S7.Net.UnitTest/ConnectionRequestTest.cs
+++ b/S7.Net.UnitTest/ConnectionRequestTest.cs
@@ -9,52 +9,52 @@ public class ConnectionRequestTest
[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 @@ private static byte[] MakeConnectionRequest(byte sourceTsap1, byte sourceTsap2,
{
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
diff --git a/S7.Net.UnitTest/ProtocolTests.cs b/S7.Net.UnitTest/ProtocolTests.cs
index 2051edbc..e63fac6c 100644
--- a/S7.Net.UnitTest/ProtocolTests.cs
+++ b/S7.Net.UnitTest/ProtocolTests.cs
@@ -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 @@ public class ProtocolUnitTest
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 @@ public async Task TPKT_ReadShortAsync()
}
[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 @@ public static byte[] StringToByteArray(string hex)
[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 });
diff --git a/S7.Net.UnitTest/S7.Net.UnitTest.csproj b/S7.Net.UnitTest/S7.Net.UnitTest.csproj
index e836b623..eb63e48c 100644
--- a/S7.Net.UnitTest/S7.Net.UnitTest.csproj
+++ b/S7.Net.UnitTest/S7.Net.UnitTest.csproj
@@ -1,7 +1,7 @@
- net452;netcoreapp3.1
+ net452;netcoreapp3.1;net5.0
true
Properties\S7.Net.snk
diff --git a/S7.Net.UnitTest/S7NetTestsAsync.cs b/S7.Net.UnitTest/S7NetTestsAsync.cs
index cb636604..f94ddb81 100644
--- a/S7.Net.UnitTest/S7NetTestsAsync.cs
+++ b/S7.Net.UnitTest/S7NetTestsAsync.cs
@@ -935,20 +935,53 @@ public async Task Test_Async_WriteLargeByteArrayWithCancellation()
{
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.");
}
+
+ ///
+ /// Write a large amount of data and test cancellation
+ ///
+ [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
+ {
+ 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
}
}
diff --git a/S7.Net.UnitTest/StreamTests.cs b/S7.Net.UnitTest/StreamTests.cs
index 9cb09690..d5fe2a1e 100644
--- a/S7.Net.UnitTest/StreamTests.cs
+++ b/S7.Net.UnitTest/StreamTests.cs
@@ -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 @@ public TestStream1BytePerRead(byte[] data)
{
Data = data;
}
+
public override bool CanRead => _position < Data.Length;
public override bool CanSeek => throw new NotImplementedException();
@@ -26,21 +28,31 @@ public TestStream1BytePerRead(byte[] data)
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 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 @@ public async Task TPKT_ReadRestrictedStreamAsync()
}
[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(() => TPKT.Read(m));
+ await Assert.ThrowsExceptionAsync(() => TPKT.ReadAsync(m, CancellationToken.None));
}
}
}
diff --git a/S7.Net/COTP.cs b/S7.Net/COTP.cs
index a7d9fd7e..3e5abbbb 100644
--- a/S7.Net/COTP.cs
+++ b/S7.Net/COTP.cs
@@ -50,22 +50,6 @@ public TPDU(TPKT tPKT)
Data = new byte[0];
}
- ///
- /// Reads COTP TPDU (Transport protocol data unit) from the network stream
- /// See: https://tools.ietf.org/html/rfc905
- ///
- /// The socket to read from
- /// COTP DPDU instance
- 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);
- }
-
///
/// Reads COTP TPDU (Transport protocol data unit) from the network stream
/// See: https://tools.ietf.org/html/rfc905
@@ -100,36 +84,6 @@ public override string ToString()
///
public class TSDU
{
- ///
- /// Reads the full COTP TSDU (Transport service data unit)
- /// See: https://tools.ietf.org/html/rfc905
- ///
- /// The stream to read from
- /// Data in TSDU
- 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;
- }
-
///
/// Reads the full COTP TSDU (Transport service data unit)
/// See: https://tools.ietf.org/html/rfc905
@@ -137,7 +91,7 @@ public static byte[] Read(Stream stream)
/// The stream to read from
/// Data in TSDU
public static async Task ReadAsync(Stream stream, CancellationToken cancellationToken)
- {
+ {
var segment = await TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
if (segment.LastDataUnit)
diff --git a/S7.Net/Enums.cs b/S7.Net/Enums.cs
index 436d94bc..fc412d3e 100644
--- a/S7.Net/Enums.cs
+++ b/S7.Net/Enums.cs
@@ -15,6 +15,11 @@ public enum CpuType
///
Logo0BA8 = 1,
+ ///
+ /// S7 200 Smart
+ ///
+ S7200Smart = 2,
+
///
/// S7 300 cpu type
///
diff --git a/S7.Net/PLC.cs b/S7.Net/PLC.cs
index 6180ad5c..35d038e0 100644
--- a/S7.Net/PLC.cs
+++ b/S7.Net/PLC.cs
@@ -15,16 +15,24 @@ namespace S7.Net
///
public partial class Plc : IDisposable
{
- private readonly TaskQueue queue = new TaskQueue();
+ ///
+ /// The default port for the S7 protocol.
+ ///
+ public const int DefaultPort = 102;
+
+ ///
+ /// The default timeout (in milliseconds) used for and .
+ ///
+ public const int DefaultTimeout = 10_000;
- private const int CONNECTION_TIMED_OUT_ERROR_CODE = 10060;
+ 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
///
/// IP address of the PLC
@@ -36,6 +44,11 @@ public partial class Plc : IDisposable
///
public int Port { get; }
+ ///
+ /// The TSAP addresses used during the connection request.
+ ///
+ public TsapPair TsapPair { get; set; }
+
///
/// CPU type of the PLC
///
@@ -108,50 +121,67 @@ public int WriteTimeout
///
/// CpuType of the PLC (select from the enum)
/// Ip address of the PLC
- /// Port address of the PLC, default 102
+ /// rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal
+ /// 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.
+ public Plc(CpuType cpu, string ip, Int16 rack, Int16 slot)
+ : this(cpu, ip, DefaultPort, rack, slot)
+ {
+ }
+
+ ///
+ /// 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.
+ ///
+ /// CpuType of the PLC (select from the enum)
+ /// Ip address of the PLC
+ /// Port number used for the connection, default 102.
/// rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal
/// 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.
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));
-
- if (string.IsNullOrEmpty(ip))
- throw new ArgumentException("IP address must valid.", nameof(ip));
+ throw new ArgumentException(
+ $"The value of argument '{nameof(cpu)}' ({cpu}) is invalid for Enum type '{typeof(CpuType).Name}'.",
+ nameof(cpu));
CPU = cpu;
- IP = ip;
- Port = port;
Rack = rack;
Slot = slot;
- MaxPDUSize = 240;
}
+
///
/// 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.
///
- /// CpuType of the PLC (select from the enum)
/// Ip address of the PLC
- /// rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal
- /// 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.
- public Plc(CpuType cpu, string ip, Int16 rack, Int16 slot)
+ /// The TSAP addresses used for the connection request.
+ public Plc(string ip, TsapPair tsapPair) : this(ip, DefaultPort, tsapPair)
{
- 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));
+ }
+ ///
+ /// 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.
+ ///
+ /// Ip address of the PLC
+ /// Port number used for the connection, default 102.
+ /// The TSAP addresses used for the connection request.
+ 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;
}
///
@@ -162,6 +192,7 @@ public void Close()
if (tcpClient != null)
{
if (tcpClient.Connected) tcpClient.Close();
+ tcpClient = null; // Can not reuse TcpClient once connection gets closed.
}
}
diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs
index 5b2d77fa..77159225 100644
--- a/S7.Net/PLCHelpers.cs
+++ b/S7.Net/PLCHelpers.cs
@@ -242,8 +242,8 @@ private void ParseDataIntoDataItems(byte[] s7data, List dataItems)
// 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++;
}
}
diff --git a/S7.Net/PlcAsynchronous.cs b/S7.Net/PlcAsynchronous.cs
index c7a5868e..a58f6293 100644
--- a/S7.Net/PlcAsynchronous.cs
+++ b/S7.Net/PlcAsynchronous.cs
@@ -37,7 +37,7 @@ await queue.Enqueue(async () =>
return default(object);
}).ConfigureAwait(false);
}
- catch(Exception)
+ catch (Exception)
{
stream.Dispose();
throw;
@@ -60,7 +60,7 @@ private async Task EstablishConnection(Stream stream, CancellationToken cancella
private async Task RequestConnection(Stream stream, CancellationToken cancellationToken)
{
- var requestData = ConnectionRequest.GetCOTPConnectionRequest(CPU, Rack, Slot);
+ var requestData = ConnectionRequest.GetCOTPConnectionRequest(TsapPair);
var response = await NoLockRequestTpduAsync(stream, requestData, cancellationToken).ConfigureAwait(false);
if (response.PDUType != COTP.PduType.ConnectionConfirmed)
@@ -438,7 +438,7 @@ public async Task WriteClassAsync(object classValue, int db, int startByteAdr =
private async Task ReadBytesWithSingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
- var dataToSend = BuildReadRequestPackage(new [] { new DataItemAddress(dataType, db, startByteAdr, count)});
+ var dataToSend = BuildReadRequestPackage(new[] { new DataItemAddress(dataType, db, startByteAdr, count) });
var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
AssertReadResponse(s7data, count);
@@ -521,22 +521,46 @@ private Task RequestTsduAsync(byte[] requestData, int offset, int length
NoLockRequestTsduAsync(stream, requestData, offset, length, cancellationToken));
}
- private static async Task NoLockRequestTpduAsync(Stream stream, byte[] requestData,
+ private async Task NoLockRequestTpduAsync(Stream stream, byte[] requestData,
CancellationToken cancellationToken = default)
{
- await stream.WriteAsync(requestData, 0, requestData.Length, cancellationToken).ConfigureAwait(false);
- var response = await COTP.TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
+ cancellationToken.ThrowIfCancellationRequested();
+ try
+ {
+ 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();
+ }
- return response;
+ throw;
+ }
}
- private static async Task NoLockRequestTsduAsync(Stream stream, byte[] requestData, int offset, int length,
+ private async Task NoLockRequestTsduAsync(Stream stream, byte[] requestData, int offset, int length,
CancellationToken cancellationToken = default)
{
- await stream.WriteAsync(requestData, offset, length, cancellationToken).ConfigureAwait(false);
- var response = await COTP.TSDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
+ 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();
+ }
- return response;
+ throw;
+ }
}
}
}
diff --git a/S7.Net/Protocol/ConnectionRequest.cs b/S7.Net/Protocol/ConnectionRequest.cs
index 64e2b5d4..9dbd3967 100644
--- a/S7.Net/Protocol/ConnectionRequest.cs
+++ b/S7.Net/Protocol/ConnectionRequest.cs
@@ -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;
}
}
diff --git a/S7.Net/Protocol/Tsap.cs b/S7.Net/Protocol/Tsap.cs
new file mode 100644
index 00000000..dc9d46c5
--- /dev/null
+++ b/S7.Net/Protocol/Tsap.cs
@@ -0,0 +1,31 @@
+namespace S7.Net.Protocol
+{
+ ///
+ /// 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.
+ ///
+ public struct Tsap
+ {
+ ///
+ /// First byte of the TSAP.
+ ///
+ public byte FirstByte { get; set; }
+
+ ///
+ /// Second byte of the TSAP.
+ ///
+ public byte SecondByte { get; set; }
+
+ ///
+ /// Initializes a new instance of the class using the specified values.
+ ///
+ /// The first byte of the TSAP.
+ /// The second byte of the TSAP.
+ public Tsap(byte firstByte, byte secondByte)
+ {
+ FirstByte = firstByte;
+ SecondByte = secondByte;
+ }
+ }
+}
\ No newline at end of file
diff --git a/S7.Net/Protocol/TsapPair.cs b/S7.Net/Protocol/TsapPair.cs
new file mode 100644
index 00000000..e54fc328
--- /dev/null
+++ b/S7.Net/Protocol/TsapPair.cs
@@ -0,0 +1,96 @@
+using System;
+
+namespace S7.Net.Protocol
+{
+ ///
+ /// Implements a pair of TSAP addresses used to connect to a PLC.
+ ///
+ public class TsapPair
+ {
+ ///
+ /// The local .
+ ///
+ public Tsap Local { get; set; }
+
+ ///
+ /// The remote
+ ///
+ public Tsap Remote { get; set; }
+
+ ///
+ /// Initializes a new instance of the class using the specified local and
+ /// remote TSAP.
+ ///
+ /// The local TSAP.
+ /// The remote TSAP.
+ public TsapPair(Tsap local, Tsap remote)
+ {
+ Local = local;
+ Remote = remote;
+ }
+
+ ///
+ /// Builds a that can be used to connect to a PLC using the default connection
+ /// addresses.
+ ///
+ ///
+ /// The remote TSAP is constructed using new Tsap(0x03, (byte) ((rack << 5) | slot))
.
+ ///
+ /// The CPU type of the PLC.
+ /// The rack of the PLC's network card.
+ /// The slot of the PLC's network card.
+ /// A TSAP pair that matches the given parameters.
+ /// The is invalid.
+ ///
+ /// -or-
+ ///
+ /// The parameter is less than 0.
+ ///
+ /// -or-
+ ///
+ /// The parameter is greater than 15.
+ ///
+ /// -or-
+ ///
+ /// The parameter is less than 0.
+ ///
+ /// -or-
+ ///
+ /// The parameter is greater than 15.
+ 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).");
+ }
+ }
+}
\ No newline at end of file
diff --git a/S7.Net/TPKT.cs b/S7.Net/TPKT.cs
index e3d26f71..a311dcec 100644
--- a/S7.Net/TPKT.cs
+++ b/S7.Net/TPKT.cs
@@ -25,34 +25,6 @@ private TPKT(byte version, byte reserved1, int length, byte[] data)
Data = data;
}
- ///
- /// Reads a TPKT from the socket
- ///
- /// The stream to read from
- /// TPKT Instance
- 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
- );
- }
-
///
/// Reads a TPKT from the socket Async
///
diff --git a/S7.sln b/S7.sln
index 2ae0e521..dbfaf5cb 100644
--- a/S7.sln
+++ b/S7.sln
@@ -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}"