mirror of
https://github.com/S7NetPlus/s7netplus.git
synced 2026-02-17 14:28:25 +08:00
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
This commit is contained in:
80
.github/workflows/test.yml
vendored
Normal file
80
.github/workflows/test.yml
vendored
Normal 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 }}
|
||||||
181
S7.Net.UnitTest/ConnectionCloseTest.cs
Normal file
181
S7.Net.UnitTest/ConnectionCloseTest.cs
Normal 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,52 +9,52 @@ namespace S7.Net.UnitTest
|
|||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Test_ConnectionRequest_S7_200()
|
public void Test_ConnectionRequest_S7_200()
|
||||||
{
|
{
|
||||||
CollectionAssert.AreEqual(MakeConnectionRequest(16, 0, 16, 0),
|
CollectionAssert.AreEqual(MakeConnectionRequest(16, 0, 16, 1),
|
||||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7200, 0, 0));
|
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7200, 0, 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Test_ConnectionRequest_S7_300()
|
public void Test_ConnectionRequest_S7_300()
|
||||||
{
|
{
|
||||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0),
|
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),
|
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),
|
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33),
|
||||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7300, 1, 1));
|
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7300, 1, 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Test_ConnectionRequest_S7_400()
|
public void Test_ConnectionRequest_S7_400()
|
||||||
{
|
{
|
||||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0),
|
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),
|
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),
|
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33),
|
||||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7400, 1, 1));
|
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7400, 1, 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Test_ConnectionRequest_S7_1200()
|
public void Test_ConnectionRequest_S7_1200()
|
||||||
{
|
{
|
||||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0),
|
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),
|
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),
|
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33),
|
||||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71200, 1, 1));
|
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71200, 1, 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Test_ConnectionRequest_S7_1500()
|
public void Test_ConnectionRequest_S7_1500()
|
||||||
{
|
{
|
||||||
CollectionAssert.AreEqual(MakeConnectionRequest(0x10, 0x2, 3, 0),
|
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0),
|
||||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71500, 0, 0));
|
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71500, 0, 0)));
|
||||||
CollectionAssert.AreEqual(MakeConnectionRequest(0x10, 0x2, 3, 1),
|
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 1),
|
||||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71500, 0, 1));
|
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71500, 0, 1)));
|
||||||
CollectionAssert.AreEqual(MakeConnectionRequest(0x10, 0x2, 3, 33),
|
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33),
|
||||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71500, 1, 1));
|
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71500, 1, 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] MakeConnectionRequest(byte sourceTsap1, byte sourceTsap2, byte destTsap1, byte destTsap2)
|
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
|
3, 0, 0, 22, //TPKT
|
||||||
17, //COTP Header Length
|
17, //COTP Header Length
|
||||||
224, //Connect Request
|
224, //Connect Request
|
||||||
0, 0, //Destination Reference
|
0, 0, //Destination Reference
|
||||||
0, 46, //Source Reference
|
0, 46, //Source Reference
|
||||||
0, //Flags
|
0, //Flags
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
using System;
|
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.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
using S7.Net.Protocol;
|
using S7.Net.Protocol;
|
||||||
using System.Collections;
|
|
||||||
|
|
||||||
namespace S7.Net.UnitTest
|
namespace S7.Net.UnitTest
|
||||||
{
|
{
|
||||||
@@ -21,21 +17,17 @@ namespace S7.Net.UnitTest
|
|||||||
public async Task TPKT_Read()
|
public async Task TPKT_Read()
|
||||||
{
|
{
|
||||||
var m = new MemoryStream(StringToByteArray("0300002902f0803203000000010002001400000401ff0400807710000100000103000000033f8ccccd"));
|
var m = new MemoryStream(StringToByteArray("0300002902f0803203000000010002001400000401ff0400807710000100000103000000033f8ccccd"));
|
||||||
var t = TPKT.Read(m);
|
var t = await TPKT.ReadAsync(m, TestContext.CancellationTokenSource.Token);
|
||||||
Assert.AreEqual(0x03, t.Version);
|
|
||||||
Assert.AreEqual(0x29, t.Length);
|
|
||||||
m.Position = 0;
|
|
||||||
t = await TPKT.ReadAsync(m, TestContext.CancellationTokenSource.Token);
|
|
||||||
Assert.AreEqual(0x03, t.Version);
|
Assert.AreEqual(0x03, t.Version);
|
||||||
Assert.AreEqual(0x29, t.Length);
|
Assert.AreEqual(0x29, t.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(TPKTInvalidException))]
|
[ExpectedException(typeof(TPKTInvalidException))]
|
||||||
public void TPKT_ReadShort()
|
public async Task TPKT_ReadShort()
|
||||||
{
|
{
|
||||||
var m = new MemoryStream(StringToByteArray("0300002902f0803203000000010002001400000401ff040080"));
|
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]
|
[TestMethod]
|
||||||
public void COTP_ReadTSDU()
|
public async Task COTP_ReadTSDU()
|
||||||
{
|
{
|
||||||
var expected = StringToByteArray("320700000400000800080001120411440100ff09000400000000");
|
var expected = StringToByteArray("320700000400000800080001120411440100ff09000400000000");
|
||||||
var m = new MemoryStream(StringToByteArray("0300000702f0000300000702f0000300002102f080320700000400000800080001120411440100ff09000400000000"));
|
var m = new MemoryStream(StringToByteArray("0300000702f0000300000702f0000300002102f080320700000400000800080001120411440100ff09000400000000"));
|
||||||
var t = COTP.TSDU.Read(m);
|
var t = await COTP.TSDU.ReadAsync(m, TestContext.CancellationTokenSource.Token);
|
||||||
Assert.IsTrue(expected.SequenceEqual(t));
|
|
||||||
m.Position = 0;
|
|
||||||
t = COTP.TSDU.ReadAsync(m, TestContext.CancellationTokenSource.Token).Result;
|
|
||||||
Assert.IsTrue(expected.SequenceEqual(t));
|
Assert.IsTrue(expected.SequenceEqual(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,14 +58,13 @@ namespace S7.Net.UnitTest
|
|||||||
|
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TestResponseCode()
|
public async Task TestResponseCode()
|
||||||
{
|
{
|
||||||
var expected = StringToByteArray("320700000400000800080001120411440100ff09000400000000");
|
var expected = StringToByteArray("320700000400000800080001120411440100ff09000400000000");
|
||||||
var m = new MemoryStream(StringToByteArray("0300000702f0000300000702f0000300002102f080320700000400000800080001120411440100ff09000400000000"));
|
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));
|
Assert.IsTrue(expected.SequenceEqual(t));
|
||||||
|
|
||||||
|
|
||||||
// Test all possible byte values. Everything except 0xff should throw an exception.
|
// 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 });
|
var testData = Enumerable.Range(0, 256).Select(i => new { StatusCode = (ReadWriteErrorCode)i, ThrowsException = i != (byte)ReadWriteErrorCode.Success });
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net452;netcoreapp3.1</TargetFrameworks>
|
<TargetFrameworks>net452;netcoreapp3.1;net5.0</TargetFrameworks>
|
||||||
|
|
||||||
<SignAssembly>true</SignAssembly>
|
<SignAssembly>true</SignAssembly>
|
||||||
<AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile>
|
<AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile>
|
||||||
|
|||||||
@@ -935,20 +935,53 @@ namespace S7.Net.UnitTest
|
|||||||
{
|
{
|
||||||
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data, cancellationToken);
|
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data, cancellationToken);
|
||||||
}
|
}
|
||||||
catch(TaskCanceledException)
|
catch(OperationCanceledException)
|
||||||
{
|
{
|
||||||
// everything is good, that is the exception we expect
|
// everything is good, that is the exception we expect
|
||||||
Console.WriteLine("Task was cancelled as expected.");
|
Console.WriteLine("Operation was cancelled as expected.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
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.
|
// 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.");
|
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
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace S7.Net.UnitTest
|
namespace S7.Net.UnitTest
|
||||||
@@ -15,6 +16,7 @@ namespace S7.Net.UnitTest
|
|||||||
{
|
{
|
||||||
Data = data;
|
Data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool CanRead => _position < Data.Length;
|
public override bool CanRead => _position < Data.Length;
|
||||||
|
|
||||||
public override bool CanSeek => throw new NotImplementedException();
|
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 override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||||
public byte[] Data { get; }
|
public byte[] Data { get; }
|
||||||
|
|
||||||
|
int _position = 0;
|
||||||
|
|
||||||
public override void Flush()
|
public override void Flush()
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
int _position = 0;
|
|
||||||
public override int Read(byte[] buffer, int offset, int count)
|
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)
|
if (_position >= Data.Length)
|
||||||
{
|
{
|
||||||
return 0;
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer[offset] = Data[_position];
|
buffer[offset] = Data[_position];
|
||||||
++_position;
|
++_position;
|
||||||
return 1;
|
|
||||||
|
return Task.FromResult(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override long Seek(long offset, SeekOrigin origin)
|
public override long Seek(long offset, SeekOrigin origin)
|
||||||
@@ -78,21 +90,21 @@ namespace S7.Net.UnitTest
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TPKT_ReadRestrictedStream()
|
public async Task TPKT_ReadRestrictedStream()
|
||||||
{
|
{
|
||||||
var fullMessage = ProtocolUnitTest.StringToByteArray("0300002902f0803203000000010002001400000401ff0400807710000100000103000000033f8ccccd");
|
var fullMessage = ProtocolUnitTest.StringToByteArray("0300002902f0803203000000010002001400000401ff0400807710000100000103000000033f8ccccd");
|
||||||
var m = new TestStream1BytePerRead(fullMessage);
|
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.Length, t.Length);
|
||||||
Assert.AreEqual(fullMessage.Last(), t.Data.Last());
|
Assert.AreEqual(fullMessage.Last(), t.Data.Last());
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TPKT_ReadStreamTooShort()
|
public async Task TPKT_ReadStreamTooShort()
|
||||||
{
|
{
|
||||||
var fullMessage = ProtocolUnitTest.StringToByteArray("0300002902f0803203000000010002001400");
|
var fullMessage = ProtocolUnitTest.StringToByteArray("0300002902f0803203000000010002001400");
|
||||||
var m = new TestStream1BytePerRead(fullMessage);
|
var m = new TestStream1BytePerRead(fullMessage);
|
||||||
Assert.ThrowsException<TPKTInvalidException>(() => TPKT.Read(m));
|
await Assert.ThrowsExceptionAsync<TPKTInvalidException>(() => TPKT.ReadAsync(m, CancellationToken.None));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,22 +50,6 @@ namespace S7.Net
|
|||||||
Data = new byte[0];
|
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>
|
/// <summary>
|
||||||
/// Reads COTP TPDU (Transport protocol data unit) from the network stream
|
/// Reads COTP TPDU (Transport protocol data unit) from the network stream
|
||||||
/// See: https://tools.ietf.org/html/rfc905
|
/// See: https://tools.ietf.org/html/rfc905
|
||||||
@@ -100,36 +84,6 @@ namespace S7.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class TSDU
|
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>
|
/// <summary>
|
||||||
/// Reads the full COTP TSDU (Transport service data unit)
|
/// Reads the full COTP TSDU (Transport service data unit)
|
||||||
/// See: https://tools.ietf.org/html/rfc905
|
/// See: https://tools.ietf.org/html/rfc905
|
||||||
@@ -137,7 +91,7 @@ namespace S7.Net
|
|||||||
/// <param name="stream">The stream to read from</param>
|
/// <param name="stream">The stream to read from</param>
|
||||||
/// <returns>Data in TSDU</returns>
|
/// <returns>Data in TSDU</returns>
|
||||||
public static async Task<byte[]> ReadAsync(Stream stream, CancellationToken cancellationToken)
|
public static async Task<byte[]> ReadAsync(Stream stream, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var segment = await TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
|
var segment = await TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (segment.LastDataUnit)
|
if (segment.LastDataUnit)
|
||||||
|
|||||||
@@ -15,6 +15,11 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Logo0BA8 = 1,
|
Logo0BA8 = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S7 200 Smart
|
||||||
|
/// </summary>
|
||||||
|
S7200Smart = 2,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// S7 300 cpu type
|
/// S7 300 cpu type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -15,16 +15,24 @@ namespace S7.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class Plc : IDisposable
|
public partial class Plc : IDisposable
|
||||||
{
|
{
|
||||||
private readonly TaskQueue queue = new TaskQueue();
|
/// <summary>
|
||||||
|
/// The default port for the S7 protocol.
|
||||||
|
/// </summary>
|
||||||
|
public const int DefaultPort = 102;
|
||||||
|
|
||||||
private const int CONNECTION_TIMED_OUT_ERROR_CODE = 10060;
|
/// <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
|
//TCP connection to device
|
||||||
private TcpClient? tcpClient;
|
private TcpClient? tcpClient;
|
||||||
private NetworkStream? _stream;
|
private NetworkStream? _stream;
|
||||||
|
|
||||||
private int readTimeout = 0; // default no timeout
|
private int readTimeout = DefaultTimeout; // default no timeout
|
||||||
private int writeTimeout = 0; // default no timeout
|
private int writeTimeout = DefaultTimeout; // default no timeout
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// IP address of the PLC
|
/// IP address of the PLC
|
||||||
@@ -36,6 +44,11 @@ namespace S7.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int Port { get; }
|
public int Port { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The TSAP addresses used during the connection request.
|
||||||
|
/// </summary>
|
||||||
|
public TsapPair TsapPair { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// CPU type of the PLC
|
/// CPU type of the PLC
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -108,25 +121,14 @@ namespace S7.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cpu">CpuType of the PLC (select from the enum)</param>
|
/// <param name="cpu">CpuType of the PLC (select from the enum)</param>
|
||||||
/// <param name="ip">Ip address of the PLC</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="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.
|
/// <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>
|
/// 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>
|
/// <summary>
|
||||||
/// Creates a PLC object with all the parameters needed for connections.
|
/// 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.
|
/// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0.
|
||||||
@@ -135,23 +137,51 @@ namespace S7.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cpu">CpuType of the PLC (select from the enum)</param>
|
/// <param name="cpu">CpuType of the PLC (select from the enum)</param>
|
||||||
/// <param name="ip">Ip address of the PLC</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="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.
|
/// <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>
|
/// 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))
|
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))
|
if (string.IsNullOrEmpty(ip))
|
||||||
throw new ArgumentException("IP address must valid.", nameof(ip));
|
throw new ArgumentException("IP address must valid.", nameof(ip));
|
||||||
|
|
||||||
CPU = cpu;
|
|
||||||
IP = ip;
|
IP = ip;
|
||||||
Port = 102;
|
Port = port;
|
||||||
Rack = rack;
|
|
||||||
Slot = slot;
|
|
||||||
MaxPDUSize = 240;
|
MaxPDUSize = 240;
|
||||||
|
TsapPair = tsapPair;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -162,6 +192,7 @@ namespace S7.Net
|
|||||||
if (tcpClient != null)
|
if (tcpClient != null)
|
||||||
{
|
{
|
||||||
if (tcpClient.Connected) tcpClient.Close();
|
if (tcpClient.Connected) tcpClient.Close();
|
||||||
|
tcpClient = null; // Can not reuse TcpClient once connection gets closed.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -242,8 +242,8 @@ namespace S7.Net
|
|||||||
// next Item
|
// next Item
|
||||||
offset += byteCnt;
|
offset += byteCnt;
|
||||||
|
|
||||||
// Fill byte in response when bytecount is odd
|
// Always align to even offset
|
||||||
if (dataItem.Count % 2 != 0 && (dataItem.VarType == VarType.Byte || dataItem.VarType == VarType.Bit))
|
if (offset % 2 != 0)
|
||||||
offset++;
|
offset++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ namespace S7.Net
|
|||||||
return default(object);
|
return default(object);
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch(Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
stream.Dispose();
|
stream.Dispose();
|
||||||
throw;
|
throw;
|
||||||
@@ -60,7 +60,7 @@ namespace S7.Net
|
|||||||
|
|
||||||
private async Task RequestConnection(Stream stream, CancellationToken cancellationToken)
|
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);
|
var response = await NoLockRequestTpduAsync(stream, requestData, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (response.PDUType != COTP.PduType.ConnectionConfirmed)
|
if (response.PDUType != COTP.PduType.ConnectionConfirmed)
|
||||||
@@ -438,7 +438,7 @@ namespace S7.Net
|
|||||||
|
|
||||||
private async Task ReadBytesWithSingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
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);
|
var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
|
||||||
AssertReadResponse(s7data, count);
|
AssertReadResponse(s7data, count);
|
||||||
@@ -521,22 +521,46 @@ namespace S7.Net
|
|||||||
NoLockRequestTsduAsync(stream, requestData, offset, length, cancellationToken));
|
NoLockRequestTsduAsync(stream, requestData, offset, length, cancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<COTP.TPDU> NoLockRequestTpduAsync(Stream stream, byte[] requestData,
|
private async Task<COTP.TPDU> NoLockRequestTpduAsync(Stream stream, byte[] requestData,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await stream.WriteAsync(requestData, 0, requestData.Length, cancellationToken).ConfigureAwait(false);
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
var response = await COTP.TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
|
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<byte[]> NoLockRequestTsduAsync(Stream stream, byte[] requestData, int offset, int length,
|
private async Task<byte[]> NoLockRequestTsduAsync(Stream stream, byte[] requestData, int offset, int length,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await stream.WriteAsync(requestData, offset, length, cancellationToken).ConfigureAwait(false);
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
var response = await COTP.TSDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +1,27 @@
|
|||||||
using System;
|
namespace S7.Net.Protocol
|
||||||
|
|
||||||
namespace S7.Net.Protocol
|
|
||||||
{
|
{
|
||||||
internal static class ConnectionRequest
|
internal static class ConnectionRequest
|
||||||
{
|
{
|
||||||
public static byte[] GetCOTPConnectionRequest(CpuType cpu, Int16 rack, Int16 slot)
|
public static byte[] GetCOTPConnectionRequest(TsapPair tsapPair)
|
||||||
{
|
{
|
||||||
byte[] bSend1 = {
|
byte[] bSend1 = {
|
||||||
3, 0, 0, 22, //TPKT
|
3, 0, 0, 22, //TPKT
|
||||||
17, //COTP Header Length
|
17, //COTP Header Length
|
||||||
224, //Connect Request
|
224, //Connect Request
|
||||||
0, 0, //Destination Reference
|
0, 0, //Destination Reference
|
||||||
0, 46, //Source Reference
|
0, 46, //Source Reference
|
||||||
0, //Flags
|
0, //Flags
|
||||||
193, //Parameter Code (src-tasp)
|
193, //Parameter Code (src-tasp)
|
||||||
2, //Parameter Length
|
2, //Parameter Length
|
||||||
1, 0, //Source TASP
|
tsapPair.Local.FirstByte, tsapPair.Local.SecondByte, //Source TASP
|
||||||
194, //Parameter Code (dst-tasp)
|
194, //Parameter Code (dst-tasp)
|
||||||
2, //Parameter Length
|
2, //Parameter Length
|
||||||
3, 0, //Destination TASP
|
tsapPair.Remote.FirstByte, tsapPair.Remote.SecondByte, //Destination TASP
|
||||||
192, //Parameter Code (tpdu-size)
|
192, //Parameter Code (tpdu-size)
|
||||||
1, //Parameter Length
|
1, //Parameter Length
|
||||||
10 //TPDU Size (2^10 = 1024)
|
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;
|
return bSend1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
31
S7.Net/Protocol/Tsap.cs
Normal file
31
S7.Net/Protocol/Tsap.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
96
S7.Net/Protocol/TsapPair.cs
Normal file
96
S7.Net/Protocol/TsapPair.cs
Normal 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 << 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).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,34 +25,6 @@ namespace S7.Net
|
|||||||
Data = data;
|
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>
|
/// <summary>
|
||||||
/// Reads a TPKT from the socket Async
|
/// Reads a TPKT from the socket Async
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
1
S7.sln
1
S7.sln
@@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
appveyor.yml = appveyor.yml
|
appveyor.yml = appveyor.yml
|
||||||
README.md = README.md
|
README.md = README.md
|
||||||
|
.github\workflows\test.yml = .github\workflows\test.yml
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "S7.Net.UnitTest", "S7.Net.UnitTest\S7.Net.UnitTest.csproj", "{303CCED6-9ABC-4899-A509-743341AAA804}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "S7.Net.UnitTest", "S7.Net.UnitTest\S7.Net.UnitTest.csproj", "{303CCED6-9ABC-4899-A509-743341AAA804}"
|
||||||
|
|||||||
Reference in New Issue
Block a user