mirror of
https://github.com/S7NetPlus/s7netplus.git
synced 2026-02-22 05:54:46 +08:00
Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43b29825a4 | ||
|
|
6aa0133081 | ||
|
|
868e719b78 | ||
|
|
3833d29c0e | ||
|
|
144f814794 | ||
|
|
82aaf7e2cb | ||
|
|
f47918946d | ||
|
|
142f1ba90e | ||
|
|
d99d0d0e6f | ||
|
|
ce9f9f9e08 | ||
|
|
ea1140314b | ||
|
|
e3fad0b94f | ||
|
|
7f76d4fc5a | ||
|
|
ec554ddb59 | ||
|
|
2ecd2c6b49 | ||
|
|
d808ea5eb6 | ||
|
|
5d3f01e59e | ||
|
|
9c3f95ce73 | ||
|
|
12281ec802 | ||
|
|
8df1a9c8cb | ||
|
|
946536c2d6 | ||
|
|
b475aee2e7 | ||
|
|
82a745c972 | ||
|
|
74fecad48d | ||
|
|
37c63cea8e | ||
|
|
36c4564f5e | ||
|
|
0c0200d12d | ||
|
|
6e861d2f00 | ||
|
|
372f4a5dcb | ||
|
|
0d87dbf3c6 | ||
|
|
15f94cd7bf | ||
|
|
36b59a2926 | ||
|
|
8d081859da | ||
|
|
2302819650 | ||
|
|
d65b83660f | ||
|
|
77b2ecfd45 | ||
|
|
b0a6a2375f | ||
|
|
14823bca96 | ||
|
|
53045c5952 | ||
|
|
677d2941e1 | ||
|
|
66b693676c | ||
|
|
4542bbedb2 | ||
|
|
a4b6a360fe | ||
|
|
0bef6bc9ff | ||
|
|
cbe04fbfb4 | ||
|
|
0b4c79cb08 | ||
|
|
5c24e801fd | ||
|
|
bcde65120c | ||
|
|
4541a7ebb7 | ||
|
|
fc9c33fdaf | ||
|
|
a23408d67e | ||
|
|
adb55dc80f | ||
|
|
2fae2c01d5 | ||
|
|
6465e3c8c7 | ||
|
|
fcd61c1236 | ||
|
|
616dc1094c | ||
|
|
70bc1499ef | ||
|
|
fd9aeb5b3b | ||
|
|
0750ee006f | ||
|
|
5318f94dd7 | ||
|
|
ea3beff481 | ||
|
|
f67b1e773f | ||
|
|
e93a656312 | ||
|
|
8035f71a16 | ||
|
|
df4f258290 | ||
|
|
5636b93a53 | ||
|
|
8ed1b840bc | ||
|
|
2afed88231 | ||
|
|
1ded47971b | ||
|
|
ced10b4eca | ||
|
|
632e1c14ac | ||
|
|
aa50280233 | ||
|
|
3a794e8a46 | ||
|
|
0b8bd66bf7 | ||
|
|
e66d21af05 | ||
|
|
44ee651ac4 | ||
|
|
a1b4694ef6 | ||
|
|
d10c15b80f | ||
|
|
5225c8bffd | ||
|
|
aa03400350 | ||
|
|
2b4ec6d9dd | ||
|
|
54f3de6c9f | ||
|
|
e6d14587d3 | ||
|
|
e63d92c61c | ||
|
|
b4b94e1777 | ||
|
|
13c25fc20b | ||
|
|
fff6f3458f | ||
|
|
82e29837a2 | ||
|
|
ae70f31af2 | ||
|
|
fb44b56c16 | ||
|
|
ce97fcf335 | ||
|
|
81208c0f03 |
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-20.04, macos-latest]
|
||||
test-framework: [netcoreapp3.1, net5.0]
|
||||
include:
|
||||
- os: ubuntu-20.04
|
||||
test-framework: netcoreapp3.1
|
||||
installSnap7: true
|
||||
dotnet-sdk: '3.1.x'
|
||||
- os: ubuntu-20.04
|
||||
test-framework: net5.0
|
||||
installSnap7: true
|
||||
dotnet-sdk: '5.0.x'
|
||||
- os: macos-latest
|
||||
test-framework: netcoreapp3.1
|
||||
installSnap7: true
|
||||
dotnet-sdk: '3.1.x'
|
||||
- os: macos-latest
|
||||
test-framework: net5.0
|
||||
installSnap7: true
|
||||
dotnet-sdk: '5.0.x'
|
||||
- os: windows-latest
|
||||
test-framework: netcoreapp3.1
|
||||
dotnet-sdk: '3.1.x'
|
||||
- os: windows-latest
|
||||
test-framework: net5.0
|
||||
dotnet-sdk: '5.0.x'
|
||||
- os: windows-latest
|
||||
test-framework: net452
|
||||
dotnet-sdk: '5.0.x'
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Snap7 Linux
|
||||
if: ${{ matrix.installSnap7 && matrix.os == 'ubuntu-20.04' }}
|
||||
run: |
|
||||
sudo add-apt-repository ppa:gijzelaar/snap7
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsnap7-1 libsnap7-dev
|
||||
|
||||
- name: Install Snap7 MacOs
|
||||
if: ${{ matrix.installSnap7 && matrix.os == 'macos-latest' }}
|
||||
run: |
|
||||
brew install snap7
|
||||
|
||||
- name: Setup Dotnet
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: ${{ matrix.dotnet-sdk }}
|
||||
|
||||
- name: Nuget Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
# Look to see if there is a cache hit for the corresponding requirements file
|
||||
key: ${{ runner.os }}-${{ matrix.test-framework }}-nuget-${{ hashFiles('**/packages.lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ matrix.test-framework }}-nuget
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore S7.Net.UnitTest
|
||||
|
||||
- name: Test
|
||||
run: dotnet test --no-restore --nologo --verbosity normal --logger GitHubActions --framework ${{ matrix.test-framework }}
|
||||
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]
|
||||
public void Test_ConnectionRequest_S7_200()
|
||||
{
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(16, 0, 16, 0),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7200, 0, 0));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(16, 0, 16, 1),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7200, 0, 0)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Test_ConnectionRequest_S7_300()
|
||||
{
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7300, 0, 0));
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7300, 0, 0)));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 1),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7300, 0, 1));
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7300, 0, 1)));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7300, 1, 1));
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7300, 1, 1)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Test_ConnectionRequest_S7_400()
|
||||
{
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7400, 0, 0));
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7400, 0, 0)));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 1),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7400, 0, 1));
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7400, 0, 1)));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7400, 1, 1));
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7400, 1, 1)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Test_ConnectionRequest_S7_1200()
|
||||
{
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71200, 0, 0));
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71200, 0, 0)));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 1),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71200, 0, 1));
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71200, 0, 1)));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71200, 1, 1));
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71200, 1, 1)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Test_ConnectionRequest_S7_1500()
|
||||
{
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(0x10, 0x2, 3, 0),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71500, 0, 0));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(0x10, 0x2, 3, 1),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71500, 0, 1));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(0x10, 0x2, 3, 33),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71500, 1, 1));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71500, 0, 0)));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 1),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71500, 0, 1)));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71500, 1, 1)));
|
||||
}
|
||||
|
||||
private static byte[] MakeConnectionRequest(byte sourceTsap1, byte sourceTsap2, byte destTsap1, byte destTsap2)
|
||||
@@ -63,7 +63,7 @@ namespace S7.Net.UnitTest
|
||||
{
|
||||
3, 0, 0, 22, //TPKT
|
||||
17, //COTP Header Length
|
||||
224, //Connect Request
|
||||
224, //Connect Request
|
||||
0, 0, //Destination Reference
|
||||
0, 46, //Source Reference
|
||||
0, //Flags
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
|
||||
using S7.Net.Types;
|
||||
|
||||
namespace S7.Net.UnitTest.Helpers
|
||||
{
|
||||
class TestClass
|
||||
@@ -51,5 +53,16 @@ namespace S7.Net.UnitTest.Helpers
|
||||
/// DB1.DBD16
|
||||
/// </summary>
|
||||
public ushort DWordVariable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// DB1.DBX20.0
|
||||
/// </summary>
|
||||
[S7String(S7StringType.S7WString, 10)]
|
||||
public string WStringVariable { get; set; }
|
||||
/// <summary>
|
||||
/// DB1.DBX44.0
|
||||
/// </summary>
|
||||
[S7String(S7StringType.S7String, 10)]
|
||||
public string StringVariable { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using S7.Net;
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using S7.Net.Protocol;
|
||||
using System.Collections;
|
||||
|
||||
namespace S7.Net.UnitTest
|
||||
{
|
||||
@@ -21,21 +17,17 @@ namespace S7.Net.UnitTest
|
||||
public async Task TPKT_Read()
|
||||
{
|
||||
var m = new MemoryStream(StringToByteArray("0300002902f0803203000000010002001400000401ff0400807710000100000103000000033f8ccccd"));
|
||||
var t = TPKT.Read(m);
|
||||
Assert.AreEqual(0x03, t.Version);
|
||||
Assert.AreEqual(0x29, t.Length);
|
||||
m.Position = 0;
|
||||
t = await TPKT.ReadAsync(m, TestContext.CancellationTokenSource.Token);
|
||||
var t = await TPKT.ReadAsync(m, TestContext.CancellationTokenSource.Token);
|
||||
Assert.AreEqual(0x03, t.Version);
|
||||
Assert.AreEqual(0x29, t.Length);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(TPKTInvalidException))]
|
||||
public void TPKT_ReadShort()
|
||||
public async Task TPKT_ReadShort()
|
||||
{
|
||||
var m = new MemoryStream(StringToByteArray("0300002902f0803203000000010002001400000401ff040080"));
|
||||
var t = TPKT.Read(m);
|
||||
var t = await TPKT.ReadAsync(m, CancellationToken.None);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,14 +40,11 @@ namespace S7.Net.UnitTest
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void COTP_ReadTSDU()
|
||||
public async Task COTP_ReadTSDU()
|
||||
{
|
||||
var expected = StringToByteArray("320700000400000800080001120411440100ff09000400000000");
|
||||
var m = new MemoryStream(StringToByteArray("0300000702f0000300000702f0000300002102f080320700000400000800080001120411440100ff09000400000000"));
|
||||
var t = COTP.TSDU.Read(m);
|
||||
Assert.IsTrue(expected.SequenceEqual(t));
|
||||
m.Position = 0;
|
||||
t = COTP.TSDU.ReadAsync(m, TestContext.CancellationTokenSource.Token).Result;
|
||||
var t = await COTP.TSDU.ReadAsync(m, TestContext.CancellationTokenSource.Token);
|
||||
Assert.IsTrue(expected.SequenceEqual(t));
|
||||
}
|
||||
|
||||
@@ -69,14 +58,13 @@ namespace S7.Net.UnitTest
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void TestResponseCode()
|
||||
public async Task TestResponseCode()
|
||||
{
|
||||
var expected = StringToByteArray("320700000400000800080001120411440100ff09000400000000");
|
||||
var m = new MemoryStream(StringToByteArray("0300000702f0000300000702f0000300002102f080320700000400000800080001120411440100ff09000400000000"));
|
||||
var t = COTP.TSDU.Read(m);
|
||||
var t = await COTP.TSDU.ReadAsync(m, CancellationToken.None);
|
||||
Assert.IsTrue(expected.SequenceEqual(t));
|
||||
|
||||
|
||||
// Test all possible byte values. Everything except 0xff should throw an exception.
|
||||
var testData = Enumerable.Range(0, 256).Select(i => new { StatusCode = (ReadWriteErrorCode)i, ThrowsException = i != (byte)ReadWriteErrorCode.Success });
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net452;netcoreapp3.1</TargetFrameworks>
|
||||
<TargetFrameworks>net452;netcoreapp3.1;net5.0</TargetFrameworks>
|
||||
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile>
|
||||
|
||||
@@ -7,6 +7,7 @@ using S7.Net.Types;
|
||||
using S7.UnitTest.Helpers;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -154,7 +155,9 @@ namespace S7.Net.UnitTest
|
||||
IntVariable = -15000,
|
||||
LRealVariable = -154.789,
|
||||
RealVariable = -154.789f,
|
||||
DWordVariable = 850
|
||||
DWordVariable = 850,
|
||||
WStringVariable = "ÄÜÉÊéà",
|
||||
StringVariable = "Hallo"
|
||||
};
|
||||
|
||||
await plc.WriteClassAsync(tc, DB2);
|
||||
@@ -168,6 +171,8 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(tc.LRealVariable, tc2.LRealVariable);
|
||||
Assert.AreEqual(tc.RealVariable, tc2.RealVariable);
|
||||
Assert.AreEqual(tc.DWordVariable, tc2.DWordVariable);
|
||||
Assert.AreEqual(tc.WStringVariable, tc2.WStringVariable);
|
||||
Assert.AreEqual(tc.StringVariable, tc2.StringVariable);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -580,7 +585,9 @@ namespace S7.Net.UnitTest
|
||||
IntVariable = -15000,
|
||||
LRealVariable = -154.789,
|
||||
RealVariable = -154.789f,
|
||||
DWordVariable = 850
|
||||
DWordVariable = 850,
|
||||
WStringVariable = "ÄÜÉÊéà",
|
||||
StringVariable = "Hallo"
|
||||
};
|
||||
|
||||
await plc.WriteClassAsync(tc, DB2);
|
||||
@@ -628,7 +635,10 @@ namespace S7.Net.UnitTest
|
||||
IntVariable = -15000,
|
||||
LRealVariable = -154.789,
|
||||
RealVariable = -154.789f,
|
||||
DWordVariable = 850
|
||||
DWordVariable = 850,
|
||||
WStringVariable = "ÄÜÉÊéà",
|
||||
StringVariable = "Hallo"
|
||||
|
||||
};
|
||||
|
||||
await plc.WriteClassAsync(tc, DB2);
|
||||
@@ -646,6 +656,9 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(Math.Round(tc2.LRealVariable, 3), Math.Round(tc2Generic.LRealVariable, 3));
|
||||
Assert.AreEqual(tc2.RealVariable, tc2Generic.RealVariable);
|
||||
Assert.AreEqual(tc2.DWordVariable, tc2Generic.DWordVariable);
|
||||
Assert.AreEqual(tc2.WStringVariable, tc2Generic.WStringVariable);
|
||||
Assert.AreEqual(tc2.StringVariable, tc2Generic.StringVariable);
|
||||
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -671,7 +684,9 @@ namespace S7.Net.UnitTest
|
||||
IntVariable = -15000,
|
||||
LRealVariable = -154.789,
|
||||
RealVariable = -154.789f,
|
||||
DWordVariable = 850
|
||||
DWordVariable = 850,
|
||||
WStringVariable = "ÄÜÉÊéà",
|
||||
StringVariable = "Hallo"
|
||||
};
|
||||
|
||||
await plc.WriteClassAsync(tc, DB2);
|
||||
@@ -686,6 +701,8 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(Math.Round(tc2Generic.LRealVariable, 3), Math.Round(tc2GenericWithClassFactory.LRealVariable, 3));
|
||||
Assert.AreEqual(tc2Generic.RealVariable, tc2GenericWithClassFactory.RealVariable);
|
||||
Assert.AreEqual(tc2Generic.DWordVariable, tc2GenericWithClassFactory.DWordVariable);
|
||||
Assert.AreEqual(tc2Generic.WStringVariable, tc2GenericWithClassFactory.WStringVariable);
|
||||
Assert.AreEqual(tc2Generic.StringVariable, tc2GenericWithClassFactory.StringVariable);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -792,7 +809,9 @@ namespace S7.Net.UnitTest
|
||||
IntVariable = -15000,
|
||||
LRealVariable = -154.789,
|
||||
RealVariable = -154.789f,
|
||||
DWordVariable = 850
|
||||
DWordVariable = 850,
|
||||
WStringVariable = "ÄÜÉÊéà",
|
||||
StringVariable = "Hallo"
|
||||
};
|
||||
plc.WriteClass(tc, DB2);
|
||||
|
||||
@@ -935,20 +954,53 @@ namespace S7.Net.UnitTest
|
||||
{
|
||||
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data, cancellationToken);
|
||||
}
|
||||
catch(TaskCanceledException)
|
||||
catch(OperationCanceledException)
|
||||
{
|
||||
// everything is good, that is the exception we expect
|
||||
Console.WriteLine("Task was cancelled as expected.");
|
||||
Console.WriteLine("Operation was cancelled as expected.");
|
||||
return;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Assert.Fail($"Wrong exception type received. Expected {typeof(TaskCanceledException)}, received {e.GetType()}.");
|
||||
Assert.Fail($"Wrong exception type received. Expected {typeof(OperationCanceledException)}, received {e.GetType()}.");
|
||||
}
|
||||
|
||||
// Depending on how tests run, this can also just succeed without getting cancelled at all. Do nothing in this case.
|
||||
Console.WriteLine("Task was not cancelled as expected.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a large amount of data and test cancellation
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Test_Async_ParseDataIntoDataItemsAlignment()
|
||||
{
|
||||
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
|
||||
|
||||
var db = 2;
|
||||
// First write a sensible S7 string capacity
|
||||
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, new byte[] {5, 0});
|
||||
|
||||
// Read two data items, with the first having odd number of bytes (7),
|
||||
// and the second has to be aligned on a even address
|
||||
var dataItems = new List<DataItem>
|
||||
{
|
||||
new DataItem
|
||||
{
|
||||
DataType = DataType.DataBlock,
|
||||
DB = db,
|
||||
VarType = VarType.S7String,
|
||||
Count = 5
|
||||
},
|
||||
new DataItem
|
||||
{
|
||||
DataType = DataType.DataBlock,
|
||||
DB = db,
|
||||
VarType = VarType.Word,
|
||||
}
|
||||
};
|
||||
await plc.ReadMultipleVarsAsync(dataItems, CancellationToken.None);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using S7.Net.UnitTest.Helpers;
|
||||
using S7.Net.Types;
|
||||
using S7.UnitTest.Helpers;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -183,6 +184,9 @@ namespace S7.Net.UnitTest
|
||||
tc.LRealVariable = -154.789;
|
||||
tc.RealVariable = -154.789f;
|
||||
tc.DWordVariable = 850;
|
||||
tc.WStringVariable = "ÄÜÉÊéà";
|
||||
tc.StringVariable = "Hallo";
|
||||
|
||||
plc.WriteClass(tc, DB2);
|
||||
TestClass tc2 = new TestClass();
|
||||
// Values that are read from a class are stored inside the class itself, that is passed by reference
|
||||
@@ -194,6 +198,8 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(tc.LRealVariable, tc2.LRealVariable);
|
||||
Assert.AreEqual(tc.RealVariable, tc2.RealVariable);
|
||||
Assert.AreEqual(tc.DWordVariable, tc2.DWordVariable);
|
||||
Assert.AreEqual(tc.WStringVariable, tc2.WStringVariable);
|
||||
Assert.AreEqual(tc.StringVariable, tc2.StringVariable);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -577,6 +583,8 @@ namespace S7.Net.UnitTest
|
||||
tc.LRealVariable = -154.789;
|
||||
tc.RealVariable = -154.789f;
|
||||
tc.DWordVariable = 850;
|
||||
tc.WStringVariable = "ÄÜÉÊéà";
|
||||
tc.StringVariable = "Hallo";
|
||||
|
||||
plc.WriteClass(tc, DB2);
|
||||
|
||||
@@ -622,6 +630,8 @@ namespace S7.Net.UnitTest
|
||||
tc.LRealVariable = -154.789;
|
||||
tc.RealVariable = -154.789f;
|
||||
tc.DWordVariable = 850;
|
||||
tc.WStringVariable = "ÄÜÉÊéà";
|
||||
tc.StringVariable = "Hallo";
|
||||
|
||||
plc.WriteClass(tc, DB2);
|
||||
|
||||
@@ -637,6 +647,8 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(Math.Round(tc2.LRealVariable, 3), Math.Round(tc2Generic.LRealVariable, 3));
|
||||
Assert.AreEqual(tc2.RealVariable, tc2Generic.RealVariable);
|
||||
Assert.AreEqual(tc2.DWordVariable, tc2Generic.DWordVariable);
|
||||
Assert.AreEqual(tc2.WStringVariable, tc2Generic.WStringVariable);
|
||||
Assert.AreEqual(tc2.StringVariable, tc2Generic.StringVariable);
|
||||
}
|
||||
|
||||
[TestMethod, ExpectedException(typeof(PlcException))]
|
||||
@@ -665,6 +677,8 @@ namespace S7.Net.UnitTest
|
||||
tc.LRealVariable = -154.789;
|
||||
tc.RealVariable = -154.789f;
|
||||
tc.DWordVariable = 850;
|
||||
tc.WStringVariable = "ÄÜÉÊéà";
|
||||
tc.StringVariable = "Hallo";
|
||||
|
||||
plc.WriteClass(tc, DB2);
|
||||
|
||||
@@ -679,6 +693,8 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(Math.Round(tc2Generic.LRealVariable, 3), Math.Round(tc2GenericWithClassFactory.LRealVariable, 3));
|
||||
Assert.AreEqual(tc2Generic.RealVariable, tc2GenericWithClassFactory.RealVariable);
|
||||
Assert.AreEqual(tc2Generic.DWordVariable, tc2GenericWithClassFactory.DWordVariable);
|
||||
Assert.AreEqual(tc2Generic.WStringVariable, tc2GenericWithClassFactory.WStringVariable);
|
||||
Assert.AreEqual(tc2Generic.StringVariable, tc2GenericWithClassFactory.StringVariable);
|
||||
}
|
||||
|
||||
[TestMethod, ExpectedException(typeof(PlcException))]
|
||||
@@ -837,6 +853,9 @@ namespace S7.Net.UnitTest
|
||||
tc.LRealVariable = -154.789;
|
||||
tc.RealVariable = -154.789f;
|
||||
tc.DWordVariable = 850;
|
||||
tc.WStringVariable = "ÄÜÉÊéà";
|
||||
tc.StringVariable = "Hallo";
|
||||
|
||||
plc.WriteClass(tc, DB2);
|
||||
|
||||
int expectedReadBytes = (int)Types.Class.GetClassSize(tc);
|
||||
@@ -933,7 +952,14 @@ namespace S7.Net.UnitTest
|
||||
S7TestServer.Stop();
|
||||
|
||||
var unreachablePlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 2);
|
||||
Assert.IsFalse(unreachablePlc.IsAvailable);
|
||||
try
|
||||
{
|
||||
unreachablePlc.Open();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
Assert.IsFalse(unreachablePlc.IsConnected);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -944,7 +970,8 @@ namespace S7.Net.UnitTest
|
||||
S7TestServer.Start(TestServerPort);
|
||||
|
||||
var reachablePlc = CreatePlc();
|
||||
Assert.IsTrue(reachablePlc.IsAvailable);
|
||||
reachablePlc.Open();
|
||||
Assert.IsTrue(reachablePlc.IsConnected);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace S7.Net.UnitTest
|
||||
@@ -15,6 +16,7 @@ namespace S7.Net.UnitTest
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public override bool CanRead => _position < Data.Length;
|
||||
|
||||
public override bool CanSeek => throw new NotImplementedException();
|
||||
@@ -26,21 +28,31 @@ namespace S7.Net.UnitTest
|
||||
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
public byte[] Data { get; }
|
||||
|
||||
int _position = 0;
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
int _position = 0;
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (_position >= Data.Length)
|
||||
{
|
||||
return 0;
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
buffer[offset] = Data[_position];
|
||||
++_position;
|
||||
return 1;
|
||||
|
||||
return Task.FromResult(1);
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
@@ -78,21 +90,21 @@ namespace S7.Net.UnitTest
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TPKT_ReadRestrictedStream()
|
||||
public async Task TPKT_ReadRestrictedStream()
|
||||
{
|
||||
var fullMessage = ProtocolUnitTest.StringToByteArray("0300002902f0803203000000010002001400000401ff0400807710000100000103000000033f8ccccd");
|
||||
var m = new TestStream1BytePerRead(fullMessage);
|
||||
var t = TPKT.Read(m);
|
||||
var t = await TPKT.ReadAsync(m, CancellationToken.None);
|
||||
Assert.AreEqual(fullMessage.Length, t.Length);
|
||||
Assert.AreEqual(fullMessage.Last(), t.Data.Last());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TPKT_ReadStreamTooShort()
|
||||
public async Task TPKT_ReadStreamTooShort()
|
||||
{
|
||||
var fullMessage = ProtocolUnitTest.StringToByteArray("0300002902f0803203000000010002001400");
|
||||
var m = new TestStream1BytePerRead(fullMessage);
|
||||
Assert.ThrowsException<TPKTInvalidException>(() => TPKT.Read(m));
|
||||
await Assert.ThrowsExceptionAsync<TPKTInvalidException>(() => TPKT.ReadAsync(m, CancellationToken.None));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
38
S7.Net.UnitTest/TypeTests/BooleanTests.cs
Normal file
38
S7.Net.UnitTest/TypeTests/BooleanTests.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Boolean = S7.Net.Types.Boolean;
|
||||
|
||||
namespace S7.Net.UnitTest.TypeTests
|
||||
{
|
||||
[TestClass]
|
||||
public class BooleanTests
|
||||
{
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(1)]
|
||||
[DataRow(2)]
|
||||
[DataRow(3)]
|
||||
[DataRow(4)]
|
||||
[DataRow(5)]
|
||||
[DataRow(6)]
|
||||
[DataRow(7)]
|
||||
public void TestValidSetBitValues(int index)
|
||||
{
|
||||
Assert.AreEqual(Math.Pow(2, index), Boolean.SetBit(0, index));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(1)]
|
||||
[DataRow(2)]
|
||||
[DataRow(3)]
|
||||
[DataRow(4)]
|
||||
[DataRow(5)]
|
||||
[DataRow(6)]
|
||||
[DataRow(7)]
|
||||
public void TestValidClearBitValues(int index)
|
||||
{
|
||||
Assert.AreEqual((byte) ((uint) Math.Pow(2, index) ^ uint.MaxValue), Boolean.ClearBit(byte.MaxValue, index));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,19 @@ namespace S7.Net.UnitTest.TypeTests
|
||||
Assert.AreEqual(Class.GetClassSize(new TestClassUnevenSize(3, 17)), 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure Uint32 is correctly parsed through ReadClass functions. Adresses issue https://github.com/S7NetPlus/s7netplus/issues/414
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestUint32Read()
|
||||
{
|
||||
var result = new TestUint32();
|
||||
var data = new byte[4] { 0, 0, 0, 5 };
|
||||
var bytesRead = Class.FromBytes(result, data);
|
||||
Assert.AreEqual(bytesRead, data.Length);
|
||||
Assert.AreEqual(5u, result.Value1);
|
||||
}
|
||||
|
||||
private class TestClassUnevenSize
|
||||
{
|
||||
public bool Bool { get; set; }
|
||||
@@ -29,5 +42,10 @@ namespace S7.Net.UnitTest.TypeTests
|
||||
Bools = new bool[bitCount];
|
||||
}
|
||||
}
|
||||
|
||||
private class TestUint32
|
||||
{
|
||||
public uint Value1 { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,13 +117,24 @@ namespace S7.Net.UnitTest.TypeTests
|
||||
AssertToByteArrayAndBackEquals("Abc", 4, 4, 3, (byte) 'A', (byte) 'b', (byte) 'c', 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void OddS7StringByteLength()
|
||||
{
|
||||
AssertVarTypeToByteLength(VarType.S7String, 1, 4);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EvenS7StringByteLength()
|
||||
{
|
||||
AssertVarTypeToByteLength(VarType.S7String, 2, 4);
|
||||
}
|
||||
|
||||
private static void AssertFromByteArrayEquals(string expected, params byte[] bytes)
|
||||
{
|
||||
var convertedString = S7String.FromByteArray(bytes);
|
||||
Assert.AreEqual(expected, convertedString);
|
||||
}
|
||||
|
||||
|
||||
private static void AssertToByteArrayAndBackEquals(string value, int reservedLength, params byte[] expected)
|
||||
{
|
||||
var convertedData = S7String.ToByteArray(value, reservedLength);
|
||||
@@ -131,5 +142,11 @@ namespace S7.Net.UnitTest.TypeTests
|
||||
var convertedBack = S7String.FromByteArray(convertedData);
|
||||
Assert.AreEqual(value, convertedBack);
|
||||
}
|
||||
|
||||
private void AssertVarTypeToByteLength(VarType varType, int count, int expectedByteLength)
|
||||
{
|
||||
var byteLength = Plc.VarTypeToByteLength(varType, count);
|
||||
Assert.AreEqual(expectedByteLength, byteLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,17 @@ namespace S7.Net.UnitTest.TypeTests
|
||||
Assert.AreEqual(expected, convertedString);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void OddS7WStringByteLength()
|
||||
{
|
||||
AssertVarTypeToByteLength(VarType.S7WString, 1, 6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EvenS7WStringByteLength()
|
||||
{
|
||||
AssertVarTypeToByteLength(VarType.S7WString, 2, 8);
|
||||
}
|
||||
|
||||
private static void AssertToByteArrayAndBackEquals(string value, int reservedLength, params byte[] expected)
|
||||
{
|
||||
@@ -130,5 +141,11 @@ namespace S7.Net.UnitTest.TypeTests
|
||||
var convertedBack = S7WString.FromByteArray(convertedData);
|
||||
Assert.AreEqual(value, convertedBack);
|
||||
}
|
||||
|
||||
private void AssertVarTypeToByteLength(VarType varType, int count, int expectedByteLength)
|
||||
{
|
||||
var byteLength = Plc.VarTypeToByteLength(varType, count);
|
||||
Assert.AreEqual(expectedByteLength, byteLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,22 +50,6 @@ namespace S7.Net
|
||||
Data = new byte[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads COTP TPDU (Transport protocol data unit) from the network stream
|
||||
/// See: https://tools.ietf.org/html/rfc905
|
||||
/// </summary>
|
||||
/// <param name="stream">The socket to read from</param>
|
||||
/// <returns>COTP DPDU instance</returns>
|
||||
public static TPDU Read(Stream stream)
|
||||
{
|
||||
var tpkt = TPKT.Read(stream);
|
||||
if (tpkt.Length == 0)
|
||||
{
|
||||
throw new TPDUInvalidException("No protocol data received");
|
||||
}
|
||||
return new TPDU(tpkt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads COTP TPDU (Transport protocol data unit) from the network stream
|
||||
/// See: https://tools.ietf.org/html/rfc905
|
||||
@@ -100,36 +84,6 @@ namespace S7.Net
|
||||
/// </summary>
|
||||
public class TSDU
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads the full COTP TSDU (Transport service data unit)
|
||||
/// See: https://tools.ietf.org/html/rfc905
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read from</param>
|
||||
/// <returns>Data in TSDU</returns>
|
||||
public static byte[] Read(Stream stream)
|
||||
{
|
||||
var segment = TPDU.Read(stream);
|
||||
|
||||
if (segment.LastDataUnit)
|
||||
{
|
||||
return segment.Data;
|
||||
}
|
||||
|
||||
// More segments are expected, prepare a buffer to store all data
|
||||
var buffer = new byte[segment.Data.Length];
|
||||
Array.Copy(segment.Data, buffer, segment.Data.Length);
|
||||
|
||||
while (!segment.LastDataUnit)
|
||||
{
|
||||
segment = TPDU.Read(stream);
|
||||
var previousLength = buffer.Length;
|
||||
Array.Resize(ref buffer, buffer.Length + segment.Data.Length);
|
||||
Array.Copy(segment.Data, 0, buffer, previousLength, segment.Data.Length);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the full COTP TSDU (Transport service data unit)
|
||||
/// See: https://tools.ietf.org/html/rfc905
|
||||
@@ -137,7 +91,7 @@ namespace S7.Net
|
||||
/// <param name="stream">The stream to read from</param>
|
||||
/// <returns>Data in TSDU</returns>
|
||||
public static async Task<byte[]> ReadAsync(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
{
|
||||
var segment = await TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (segment.LastDataUnit)
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
/// </summary>
|
||||
Logo0BA8 = 1,
|
||||
|
||||
/// <summary>
|
||||
/// S7 200 Smart
|
||||
/// </summary>
|
||||
S7200Smart = 2,
|
||||
|
||||
/// <summary>
|
||||
/// S7 300 cpu type
|
||||
/// </summary>
|
||||
|
||||
28
S7.Net/Internal/TaskQueue.cs
Normal file
28
S7.Net/Internal/TaskQueue.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace S7.Net.Internal
|
||||
{
|
||||
internal class TaskQueue
|
||||
{
|
||||
private static readonly object Sentinel = new object();
|
||||
|
||||
private Task prev = Task.FromResult(Sentinel);
|
||||
|
||||
public async Task<T> Enqueue<T>(Func<Task<T>> action)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<object>();
|
||||
await Interlocked.Exchange(ref prev, tcs.Task).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
return await action.Invoke().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
tcs.SetResult(Sentinel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
172
S7.Net/PLC.cs
172
S7.Net/PLC.cs
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using S7.Net.Internal;
|
||||
using S7.Net.Protocol;
|
||||
using S7.Net.Types;
|
||||
|
||||
@@ -13,14 +15,24 @@ namespace S7.Net
|
||||
/// </summary>
|
||||
public partial class Plc : IDisposable
|
||||
{
|
||||
private const int CONNECTION_TIMED_OUT_ERROR_CODE = 10060;
|
||||
|
||||
/// <summary>
|
||||
/// The default port for the S7 protocol.
|
||||
/// </summary>
|
||||
public const int DefaultPort = 102;
|
||||
|
||||
/// <summary>
|
||||
/// The default timeout (in milliseconds) used for <see cref="P:ReadTimeout"/> and <see cref="P:WriteTimeout"/>.
|
||||
/// </summary>
|
||||
public const int DefaultTimeout = 10_000;
|
||||
|
||||
private readonly TaskQueue queue = new TaskQueue();
|
||||
|
||||
//TCP connection to device
|
||||
private TcpClient? tcpClient;
|
||||
private NetworkStream? _stream;
|
||||
|
||||
private int readTimeout = 0; // default no timeout
|
||||
private int writeTimeout = 0; // default no timeout
|
||||
private int readTimeout = DefaultTimeout; // default no timeout
|
||||
private int writeTimeout = DefaultTimeout; // default no timeout
|
||||
|
||||
/// <summary>
|
||||
/// IP address of the PLC
|
||||
@@ -32,6 +44,11 @@ namespace S7.Net
|
||||
/// </summary>
|
||||
public int Port { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The TSAP addresses used during the connection request.
|
||||
/// </summary>
|
||||
public TsapPair TsapPair { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// CPU type of the PLC
|
||||
/// </summary>
|
||||
@@ -75,75 +92,27 @@ namespace S7.Net
|
||||
if (tcpClient != null) tcpClient.SendTimeout = writeTimeout;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if a connection to the PLC can be established
|
||||
/// </summary>
|
||||
public bool IsAvailable
|
||||
{
|
||||
//TODO: Fix This
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
OpenAsync().GetAwaiter().GetResult();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the socket is connected and polls the other peer (the PLC) to see if it's connected.
|
||||
/// This is the variable that you should continously check to see if the communication is working
|
||||
/// See also: http://stackoverflow.com/questions/2661764/how-to-check-if-a-socket-is-connected-disconnected-in-c
|
||||
/// Gets a value indicating whether a connection to the PLC has been established.
|
||||
/// </summary>
|
||||
public bool IsConnected
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
if (tcpClient == null)
|
||||
return false;
|
||||
/// <remarks>
|
||||
/// The <see cref="IsConnected"/> property gets the connection state of the Client socket as
|
||||
/// of the last I/O operation. When it returns <c>false</c>, the Client socket was either
|
||||
/// never connected, or is no longer connected.
|
||||
///
|
||||
/// <para>
|
||||
/// Because the <see cref="IsConnected"/> property only reflects the state of the connection
|
||||
/// as of the most recent operation, you should attempt to send or receive a message to
|
||||
/// determine the current state. After the message send fails, this property no longer
|
||||
/// returns <c>true</c>. Note that this behavior is by design. You cannot reliably test the
|
||||
/// state of the connection because, in the time between the test and a send/receive, the
|
||||
/// connection could have been lost. Your code should assume the socket is connected, and
|
||||
/// gracefully handle failed transmissions.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public bool IsConnected => tcpClient?.Connected ?? false;
|
||||
|
||||
//TODO: Actually check communication by sending an empty TPDU
|
||||
return tcpClient.Connected;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <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="cpu">CpuType of the PLC (select from the enum)</param>
|
||||
/// <param name="ip">Ip address of the PLC</param>
|
||||
/// <param name="port">Port address of the PLC, default 102</param>
|
||||
/// <param name="rack">rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal</param>
|
||||
/// <param name="slot">slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500.
|
||||
/// If you use an external ethernet card, this must be set accordingly.</param>
|
||||
public Plc(CpuType cpu, string ip, int port, Int16 rack, Int16 slot)
|
||||
{
|
||||
if (!Enum.IsDefined(typeof(CpuType), cpu))
|
||||
throw new ArgumentException($"The value of argument '{nameof(cpu)}' ({cpu}) is invalid for Enum type '{typeof(CpuType).Name}'.", nameof(cpu));
|
||||
|
||||
if (string.IsNullOrEmpty(ip))
|
||||
throw new ArgumentException("IP address must valid.", nameof(ip));
|
||||
|
||||
CPU = cpu;
|
||||
IP = ip;
|
||||
Port = port;
|
||||
Rack = rack;
|
||||
Slot = slot;
|
||||
MaxPDUSize = 240;
|
||||
}
|
||||
/// <summary>
|
||||
/// Creates a PLC object with all the parameters needed for connections.
|
||||
/// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0.
|
||||
@@ -156,19 +125,63 @@ namespace S7.Net
|
||||
/// <param name="slot">slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500.
|
||||
/// If you use an external ethernet card, this must be set accordingly.</param>
|
||||
public Plc(CpuType cpu, string ip, Int16 rack, Int16 slot)
|
||||
: this(cpu, ip, DefaultPort, rack, 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="cpu">CpuType of the PLC (select from the enum)</param>
|
||||
/// <param name="ip">Ip address of the PLC</param>
|
||||
/// <param name="port">Port number used for the connection, default 102.</param>
|
||||
/// <param name="rack">rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal</param>
|
||||
/// <param name="slot">slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500.
|
||||
/// If you use an external ethernet card, this must be set accordingly.</param>
|
||||
public Plc(CpuType cpu, string ip, int port, Int16 rack, Int16 slot)
|
||||
: this(ip, port, TsapPair.GetDefaultTsapPair(cpu, rack, slot))
|
||||
{
|
||||
if (!Enum.IsDefined(typeof(CpuType), cpu))
|
||||
throw new ArgumentException($"The value of argument '{nameof(cpu)}' ({cpu}) is invalid for Enum type '{typeof(CpuType).Name}'.", nameof(cpu));
|
||||
throw new ArgumentException(
|
||||
$"The value of argument '{nameof(cpu)}' ({cpu}) is invalid for Enum type '{typeof(CpuType).Name}'.",
|
||||
nameof(cpu));
|
||||
|
||||
CPU = cpu;
|
||||
Rack = rack;
|
||||
Slot = slot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a PLC object with all the parameters needed for connections.
|
||||
/// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0.
|
||||
/// You need slot > 0 if you are connecting to external ethernet card (CP).
|
||||
/// For S7-300 and S7-400 the default is rack = 0 and slot = 2.
|
||||
/// </summary>
|
||||
/// <param name="ip">Ip address of the PLC</param>
|
||||
/// <param name="tsapPair">The TSAP addresses used for the connection request.</param>
|
||||
public Plc(string ip, TsapPair tsapPair) : this(ip, DefaultPort, tsapPair)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a PLC object with all the parameters needed for connections. Use this constructor
|
||||
/// if you want to manually override the TSAP addresses used during the connection request.
|
||||
/// </summary>
|
||||
/// <param name="ip">Ip address of the PLC</param>
|
||||
/// <param name="port">Port number used for the connection, default 102.</param>
|
||||
/// <param name="tsapPair">The TSAP addresses used for the connection request.</param>
|
||||
public Plc(string ip, int port, TsapPair tsapPair)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ip))
|
||||
throw new ArgumentException("IP address must valid.", nameof(ip));
|
||||
|
||||
CPU = cpu;
|
||||
IP = ip;
|
||||
Port = 102;
|
||||
Rack = rack;
|
||||
Slot = slot;
|
||||
Port = port;
|
||||
MaxPDUSize = 240;
|
||||
TsapPair = tsapPair;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -179,6 +192,7 @@ namespace S7.Net
|
||||
if (tcpClient != null)
|
||||
{
|
||||
if (tcpClient.Connected) tcpClient.Close();
|
||||
tcpClient = null; // Can not reuse TcpClient once connection gets closed.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,6 +277,16 @@ namespace S7.Net
|
||||
}
|
||||
}
|
||||
|
||||
private Stream GetStreamIfAvailable()
|
||||
{
|
||||
if (_stream == null)
|
||||
{
|
||||
throw new PlcException(ErrorCode.ConnectionError, "Plc is not connected");
|
||||
}
|
||||
|
||||
return _stream;
|
||||
}
|
||||
|
||||
#region IDisposable Support
|
||||
private bool disposedValue = false; // To detect redundant calls
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ namespace S7.Net
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the bytes-package to request data from the PLC. You have to specify the memory type (dataType),
|
||||
/// the address of the memory, the address of the byte and the bytes count.
|
||||
/// Create the bytes-package to request data from the PLC. You have to specify the memory type (dataType),
|
||||
/// the address of the memory, the address of the byte and the bytes count.
|
||||
/// </summary>
|
||||
/// <param name="dataType">MemoryType (DB, Timer, Counter, etc.)</param>
|
||||
/// <param name="db">Address of the memory to be read</param>
|
||||
@@ -184,13 +184,15 @@ namespace S7.Net
|
||||
switch (varType)
|
||||
{
|
||||
case VarType.Bit:
|
||||
return varCount + 7 / 8;
|
||||
return (varCount + 7) / 8;
|
||||
case VarType.Byte:
|
||||
return (varCount < 1) ? 1 : varCount;
|
||||
case VarType.String:
|
||||
return varCount;
|
||||
case VarType.S7String:
|
||||
return varCount + 2;
|
||||
return ((varCount + 2) & 1) == 1 ? (varCount + 3) : (varCount + 2);
|
||||
case VarType.S7WString:
|
||||
return (varCount * 2) + 4;
|
||||
case VarType.Word:
|
||||
case VarType.Timer:
|
||||
case VarType.Int:
|
||||
@@ -240,8 +242,8 @@ namespace S7.Net
|
||||
// next Item
|
||||
offset += byteCnt;
|
||||
|
||||
// Fill byte in response when bytecount is odd
|
||||
if (dataItem.Count % 2 != 0 && (dataItem.VarType == VarType.Byte || dataItem.VarType == VarType.Bit))
|
||||
// Always align to even offset
|
||||
if (offset % 2 != 0)
|
||||
offset++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using S7.Net.Types;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using S7.Net.Protocol;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using S7.Net.Protocol.S7;
|
||||
|
||||
@@ -25,51 +25,62 @@ namespace S7.Net
|
||||
/// <returns>A task that represents the asynchronous open operation.</returns>
|
||||
public async Task OpenAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var stream = await ConnectAsync().ConfigureAwait(false);
|
||||
var stream = await ConnectAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await EstablishConnection(stream, cancellationToken).ConfigureAwait(false);
|
||||
_stream = stream;
|
||||
await queue.Enqueue(async () =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await EstablishConnection(stream, cancellationToken).ConfigureAwait(false);
|
||||
_stream = stream;
|
||||
|
||||
return default(object);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch(Exception)
|
||||
catch (Exception)
|
||||
{
|
||||
stream.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<NetworkStream> ConnectAsync()
|
||||
private async Task<NetworkStream> ConnectAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
tcpClient = new TcpClient();
|
||||
ConfigureConnection();
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
await tcpClient.ConnectAsync(IP, Port, cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
await tcpClient.ConnectAsync(IP, Port).ConfigureAwait(false);
|
||||
#endif
|
||||
return tcpClient.GetStream();
|
||||
}
|
||||
|
||||
private async Task EstablishConnection(NetworkStream stream, CancellationToken cancellationToken)
|
||||
private async Task EstablishConnection(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
await RequestConnection(stream, cancellationToken).ConfigureAwait(false);
|
||||
await SetupConnection(stream, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task RequestConnection(NetworkStream stream, CancellationToken cancellationToken)
|
||||
private async Task RequestConnection(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
var requestData = ConnectionRequest.GetCOTPConnectionRequest(CPU, Rack, Slot);
|
||||
await stream.WriteAsync(requestData, 0, requestData.Length).ConfigureAwait(false);
|
||||
var response = await COTP.TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
var requestData = ConnectionRequest.GetCOTPConnectionRequest(TsapPair);
|
||||
var response = await NoLockRequestTpduAsync(stream, requestData, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.PDUType != COTP.PduType.ConnectionConfirmed)
|
||||
{
|
||||
throw new InvalidDataException("Connection request was denied", response.TPkt.Data, 1, 0x0d);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetupConnection(NetworkStream stream, CancellationToken cancellationToken)
|
||||
private async Task SetupConnection(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
var setupData = GetS7ConnectionSetup();
|
||||
await stream.WriteAsync(setupData, 0, setupData.Length).ConfigureAwait(false);
|
||||
|
||||
var s7data = await COTP.TSDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
var s7data = await NoLockRequestTsduAsync(stream, setupData, 0, setupData.Length, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (s7data.Length < 2)
|
||||
throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup");
|
||||
|
||||
@@ -104,7 +115,7 @@ namespace S7.Net
|
||||
{
|
||||
//This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0.
|
||||
var maxToRead = Math.Min(count, MaxPDUSize - 18);
|
||||
await ReadBytesWithSingleRequestAsync(dataType, db, startByteAdr + index, resultBytes, index, maxToRead, cancellationToken);
|
||||
await ReadBytesWithSingleRequestAsync(dataType, db, startByteAdr + index, resultBytes, index, maxToRead, cancellationToken).ConfigureAwait(false);
|
||||
count -= maxToRead;
|
||||
index += maxToRead;
|
||||
}
|
||||
@@ -112,7 +123,7 @@ namespace S7.Net
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read and decode a certain number of bytes of the "VarType" provided.
|
||||
/// Read and decode a certain number of bytes of the "VarType" provided.
|
||||
/// This can be used to read multiple consecutive variables of the same type (Word, DWord, Int, etc).
|
||||
/// If the read was not successful, check LastErrorCode or LastErrorString.
|
||||
/// </summary>
|
||||
@@ -127,7 +138,7 @@ namespace S7.Net
|
||||
public async Task<object?> ReadAsync(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
int cntBytes = VarTypeToByteLength(varType, varCount);
|
||||
byte[] bytes = await ReadBytesAsync(dataType, db, startByteAdr, cntBytes, cancellationToken);
|
||||
byte[] bytes = await ReadBytesAsync(dataType, db, startByteAdr, cntBytes, cancellationToken).ConfigureAwait(false);
|
||||
return ParseBytes(varType, bytes, varCount, bitAdr);
|
||||
}
|
||||
|
||||
@@ -142,7 +153,7 @@ namespace S7.Net
|
||||
public async Task<object?> ReadAsync(string variable, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var adr = new PLCAddress(variable);
|
||||
return await ReadAsync(adr.DataType, adr.DbNumber, adr.StartByte, adr.VarType, 1, (byte)adr.BitNumber, cancellationToken);
|
||||
return await ReadAsync(adr.DataType, adr.DbNumber, adr.StartByte, adr.VarType, 1, (byte)adr.BitNumber, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -158,7 +169,7 @@ namespace S7.Net
|
||||
{
|
||||
int numBytes = Types.Struct.GetStructSize(structType);
|
||||
// now read the package
|
||||
var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes, cancellationToken);
|
||||
var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// and decode it
|
||||
return Types.Struct.FromBytes(structType, resultBytes);
|
||||
@@ -175,14 +186,14 @@ namespace S7.Net
|
||||
/// <returns>Returns a nulable struct. If nothing was read null will be returned.</returns>
|
||||
public async Task<T?> ReadStructAsync<T>(int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : struct
|
||||
{
|
||||
return await ReadStructAsync(typeof(T), db, startByteAdr, cancellationToken) as T?;
|
||||
return await ReadStructAsync(typeof(T), db, startByteAdr, cancellationToken).ConfigureAwait(false) as T?;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
|
||||
/// </summary>
|
||||
/// <param name="sourceClass">Instance of the class that will store the values</param>
|
||||
/// <param name="sourceClass">Instance of the class that will store the values</param>
|
||||
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
|
||||
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
@@ -197,7 +208,7 @@ namespace S7.Net
|
||||
}
|
||||
|
||||
// now read the package
|
||||
var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes, cancellationToken);
|
||||
var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes, cancellationToken).ConfigureAwait(false);
|
||||
// and decode it
|
||||
Class.FromBytes(sourceClass, resultBytes);
|
||||
|
||||
@@ -205,7 +216,7 @@ namespace S7.Net
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified. To instantiate the class defined by the generic
|
||||
/// type, the class needs a default constructor.
|
||||
/// </summary>
|
||||
@@ -217,11 +228,11 @@ namespace S7.Net
|
||||
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
|
||||
public async Task<T?> ReadClassAsync<T>(int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : class
|
||||
{
|
||||
return await ReadClassAsync(() => Activator.CreateInstance<T>(), db, startByteAdr, cancellationToken);
|
||||
return await ReadClassAsync(() => Activator.CreateInstance<T>(), db, startByteAdr, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class that will be instantiated</typeparam>
|
||||
@@ -234,7 +245,7 @@ namespace S7.Net
|
||||
public async Task<T?> ReadClassAsync<T>(Func<T> classFactory, int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : class
|
||||
{
|
||||
var instance = classFactory();
|
||||
var res = await ReadClassAsync(instance, db, startByteAdr, cancellationToken);
|
||||
var res = await ReadClassAsync(instance, db, startByteAdr, cancellationToken).ConfigureAwait(false);
|
||||
int readBytes = res.Item1;
|
||||
if (readBytes <= 0)
|
||||
{
|
||||
@@ -245,10 +256,10 @@ namespace S7.Net
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads multiple vars in a single request.
|
||||
/// Reads multiple vars in a single request.
|
||||
/// You have to create and pass a list of DataItems and you obtain in response the same list with the values.
|
||||
/// Values are stored in the property "Value" of the dataItem and are already converted.
|
||||
/// If you don't want the conversion, just create a dataItem of bytes.
|
||||
/// If you don't want the conversion, just create a dataItem of bytes.
|
||||
/// The number of DataItems as well as the total size of the requested data can not exceed a certain limit (protocol restriction).
|
||||
/// </summary>
|
||||
/// <param name="dataItems">List of dataitems that contains the list of variables that must be read.</param>
|
||||
@@ -256,18 +267,15 @@ namespace S7.Net
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
public async Task<List<DataItem>> ReadMultipleVarsAsync(List<DataItem> dataItems, CancellationToken cancellationToken = default)
|
||||
{
|
||||
//Snap7 seems to choke on PDU sizes above 256 even if snap7
|
||||
//Snap7 seems to choke on PDU sizes above 256 even if snap7
|
||||
//replies with bigger PDU size in connection setup.
|
||||
AssertPduSizeForRead(dataItems);
|
||||
|
||||
var stream = GetStreamIfAvailable();
|
||||
|
||||
try
|
||||
{
|
||||
var dataToSend = BuildReadRequestPackage(dataItems.Select(d => DataItem.GetDataItemAddress(d)).ToList());
|
||||
await stream.WriteAsync(dataToSend, 0, dataToSend.Length);
|
||||
var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
|
||||
|
||||
var s7data = await COTP.TSDU.ReadAsync(stream, cancellationToken);
|
||||
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
|
||||
|
||||
ParseDataIntoDataItems(s7data, dataItems);
|
||||
@@ -306,7 +314,7 @@ namespace S7.Net
|
||||
while (count > 0)
|
||||
{
|
||||
var maxToWrite = (int)Math.Min(count, MaxPDUSize - 35);
|
||||
await WriteBytesWithASingleRequestAsync(dataType, db, startByteAdr + localIndex, value, localIndex, maxToWrite, cancellationToken);
|
||||
await WriteBytesWithASingleRequestAsync(dataType, db, startByteAdr + localIndex, value, localIndex, maxToWrite, cancellationToken).ConfigureAwait(false);
|
||||
count -= maxToWrite;
|
||||
localIndex += maxToWrite;
|
||||
}
|
||||
@@ -328,7 +336,7 @@ namespace S7.Net
|
||||
if (bitAdr < 0 || bitAdr > 7)
|
||||
throw new InvalidAddressException(string.Format("Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid", bitAdr));
|
||||
|
||||
await WriteBitWithASingleRequestAsync(dataType, db, startByteAdr, bitAdr, value, cancellationToken);
|
||||
await WriteBitWithASingleRequestAsync(dataType, db, startByteAdr, bitAdr, value, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -347,7 +355,7 @@ namespace S7.Net
|
||||
if (value < 0 || value > 1)
|
||||
throw new ArgumentException("Value must be 0 or 1", nameof(value));
|
||||
|
||||
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, value == 1, cancellationToken);
|
||||
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, value == 1, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -370,7 +378,7 @@ namespace S7.Net
|
||||
//Must be writing a bit value as bitAdr is specified
|
||||
if (value is bool boolean)
|
||||
{
|
||||
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, boolean, cancellationToken);
|
||||
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, boolean, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (value is int intValue)
|
||||
{
|
||||
@@ -380,11 +388,11 @@ namespace S7.Net
|
||||
"Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid",
|
||||
bitAdr), nameof(bitAdr));
|
||||
|
||||
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, intValue == 1, cancellationToken);
|
||||
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, intValue == 1, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else throw new ArgumentException("Value must be a bool or an int to write a bit", nameof(value));
|
||||
}
|
||||
else await WriteBytesAsync(dataType, db, startByteAdr, Serialization.SerializeValue(value), cancellationToken);
|
||||
else await WriteBytesAsync(dataType, db, startByteAdr, Serialization.SerializeValue(value), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -399,7 +407,7 @@ namespace S7.Net
|
||||
public async Task WriteAsync(string variable, object value, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var adr = new PLCAddress(variable);
|
||||
await WriteAsync(adr.DataType, adr.DbNumber, adr.StartByte, value, adr.BitNumber, cancellationToken);
|
||||
await WriteAsync(adr.DataType, adr.DbNumber, adr.StartByte, value, adr.BitNumber, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -414,7 +422,7 @@ namespace S7.Net
|
||||
public async Task WriteStructAsync(object structValue, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var bytes = Struct.ToBytes(structValue).ToList();
|
||||
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes.ToArray(), cancellationToken);
|
||||
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes.ToArray(), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -430,17 +438,14 @@ namespace S7.Net
|
||||
{
|
||||
byte[] bytes = new byte[(int)Class.GetClassSize(classValue)];
|
||||
Types.Class.ToBytes(classValue, bytes);
|
||||
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes, cancellationToken);
|
||||
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task ReadBytesWithSingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
var stream = GetStreamIfAvailable();
|
||||
var dataToSend = BuildReadRequestPackage(new[] { new DataItemAddress(dataType, db, startByteAdr, count) });
|
||||
|
||||
var dataToSend = BuildReadRequestPackage(new [] { new DataItemAddress(dataType, db, startByteAdr, count)});
|
||||
await stream.WriteAsync(dataToSend, 0, dataToSend.Length, cancellationToken);
|
||||
|
||||
var s7data = await COTP.TSDU.ReadAsync(stream, cancellationToken);
|
||||
var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
|
||||
AssertReadResponse(s7data, count);
|
||||
|
||||
Array.Copy(s7data, 18, buffer, offset, count);
|
||||
@@ -456,13 +461,11 @@ namespace S7.Net
|
||||
{
|
||||
AssertPduSizeForWrite(dataItems);
|
||||
|
||||
var stream = GetStreamIfAvailable();
|
||||
|
||||
var message = new ByteArray();
|
||||
var length = S7WriteMultiple.CreateRequest(message, dataItems);
|
||||
await stream.WriteAsync(message.Array, 0, length).ConfigureAwait(false);
|
||||
|
||||
var response = await COTP.TSDU.ReadAsync(stream, CancellationToken.None).ConfigureAwait(false);
|
||||
var response = await RequestTsduAsync(message.Array, 0, length).ConfigureAwait(false);
|
||||
|
||||
S7WriteMultiple.ParseResponse(response, response.Length, dataItems);
|
||||
}
|
||||
|
||||
@@ -476,15 +479,11 @@ namespace S7.Net
|
||||
/// <returns>A task that represents the asynchronous write operation.</returns>
|
||||
private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
try
|
||||
{
|
||||
var stream = GetStreamIfAvailable();
|
||||
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value, dataOffset, count);
|
||||
var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await stream.WriteAsync(dataToSend, 0, dataToSend.Length, cancellationToken);
|
||||
|
||||
var s7data = await COTP.TSDU.ReadAsync(stream, cancellationToken);
|
||||
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@@ -499,15 +498,11 @@ namespace S7.Net
|
||||
|
||||
private async Task WriteBitWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue, CancellationToken cancellationToken)
|
||||
{
|
||||
var stream = GetStreamIfAvailable();
|
||||
|
||||
try
|
||||
{
|
||||
var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr);
|
||||
var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await stream.WriteAsync(dataToSend, 0, dataToSend.Length);
|
||||
|
||||
var s7data = await COTP.TSDU.ReadAsync(stream, cancellationToken);
|
||||
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@@ -520,13 +515,57 @@ namespace S7.Net
|
||||
}
|
||||
}
|
||||
|
||||
private Stream GetStreamIfAvailable()
|
||||
private Task<byte[]> RequestTsduAsync(byte[] requestData, CancellationToken cancellationToken = default) =>
|
||||
RequestTsduAsync(requestData, 0, requestData.Length, cancellationToken);
|
||||
|
||||
private Task<byte[]> RequestTsduAsync(byte[] requestData, int offset, int length, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_stream == null)
|
||||
var stream = GetStreamIfAvailable();
|
||||
|
||||
return queue.Enqueue(() =>
|
||||
NoLockRequestTsduAsync(stream, requestData, offset, length, cancellationToken));
|
||||
}
|
||||
|
||||
private async Task<COTP.TPDU> NoLockRequestTpduAsync(Stream stream, byte[] requestData,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
throw new PlcException(ErrorCode.ConnectionError, "Plc is not connected");
|
||||
using var closeOnCancellation = cancellationToken.Register(Close);
|
||||
await stream.WriteAsync(requestData, 0, requestData.Length, cancellationToken).ConfigureAwait(false);
|
||||
return await COTP.TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
if (exc is TPDUInvalidException || exc is TPKTInvalidException)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<byte[]> NoLockRequestTsduAsync(Stream stream, byte[] requestData, int offset, int length,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
using var closeOnCancellation = cancellationToken.Register(Close);
|
||||
await stream.WriteAsync(requestData, offset, length, cancellationToken).ConfigureAwait(false);
|
||||
return await COTP.TSDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
if (exc is TPDUInvalidException || exc is TPKTInvalidException)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
return _stream;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using S7.Net.Protocol;
|
||||
using S7.Net.Helper;
|
||||
|
||||
@@ -54,7 +52,7 @@ namespace S7.Net
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read and decode a certain number of bytes of the "VarType" provided.
|
||||
/// Read and decode a certain number of bytes of the "VarType" provided.
|
||||
/// This can be used to read multiple consecutive variables of the same type (Word, DWord, Int, etc).
|
||||
/// If the read was not successful, check LastErrorCode or LastErrorString.
|
||||
/// </summary>
|
||||
@@ -115,10 +113,10 @@ namespace S7.Net
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
|
||||
/// </summary>
|
||||
/// <param name="sourceClass">Instance of the class that will store the values</param>
|
||||
/// <param name="sourceClass">Instance of the class that will store the values</param>
|
||||
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
|
||||
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
|
||||
/// <returns>The number of read bytes</returns>
|
||||
@@ -138,7 +136,7 @@ namespace S7.Net
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified. To instantiate the class defined by the generic
|
||||
/// type, the class needs a default constructor.
|
||||
/// </summary>
|
||||
@@ -152,7 +150,7 @@ namespace S7.Net
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class that will be instantiated</typeparam>
|
||||
@@ -186,7 +184,7 @@ namespace S7.Net
|
||||
while (count > 0)
|
||||
{
|
||||
//TODO: Figure out how to use MaxPDUSize here
|
||||
//Snap7 seems to choke on PDU sizes above 256 even if snap7
|
||||
//Snap7 seems to choke on PDU sizes above 256 even if snap7
|
||||
//replies with bigger PDU size in connection setup.
|
||||
var maxToWrite = Math.Min(count, MaxPDUSize - 28);//TODO tested only when the MaxPDUSize is 480
|
||||
WriteBytesWithASingleRequest(dataType, db, startByteAdr + localIndex, value, localIndex, maxToWrite);
|
||||
@@ -298,7 +296,6 @@ namespace S7.Net
|
||||
|
||||
private void ReadBytesWithSingleRequest(DataType dataType, int db, int startByteAdr, byte[] buffer, int offset, int count)
|
||||
{
|
||||
var stream = GetStreamIfAvailable();
|
||||
try
|
||||
{
|
||||
// first create the header
|
||||
@@ -309,9 +306,7 @@ namespace S7.Net
|
||||
BuildReadDataRequestPackage(package, dataType, db, startByteAdr, count);
|
||||
|
||||
var dataToSend = package.ToArray();
|
||||
stream.Write(dataToSend, 0, dataToSend.Length);
|
||||
|
||||
var s7data = COTP.TSDU.Read(stream);
|
||||
var s7data = RequestTsdu(dataToSend);
|
||||
AssertReadResponse(s7data, count);
|
||||
|
||||
Array.Copy(s7data, 18, buffer, offset, count);
|
||||
@@ -331,13 +326,11 @@ namespace S7.Net
|
||||
{
|
||||
AssertPduSizeForWrite(dataItems);
|
||||
|
||||
var stream = GetStreamIfAvailable();
|
||||
|
||||
var message = new ByteArray();
|
||||
var length = S7WriteMultiple.CreateRequest(message, dataItems);
|
||||
stream.Write(message.Array, 0, length);
|
||||
var response = RequestTsdu(message.Array, 0, length);
|
||||
|
||||
var response = COTP.TSDU.Read(stream);
|
||||
S7WriteMultiple.ParseResponse(response, response.Length, dataItems);
|
||||
}
|
||||
|
||||
@@ -345,12 +338,9 @@ namespace S7.Net
|
||||
{
|
||||
try
|
||||
{
|
||||
var stream = GetStreamIfAvailable();
|
||||
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value, dataOffset, count);
|
||||
var s7data = RequestTsdu(dataToSend);
|
||||
|
||||
stream.Write(dataToSend, 0, dataToSend.Length);
|
||||
|
||||
var s7data = COTP.TSDU.Read(stream);
|
||||
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
|
||||
}
|
||||
catch (Exception exc)
|
||||
@@ -425,14 +415,11 @@ namespace S7.Net
|
||||
|
||||
private void WriteBitWithASingleRequest(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue)
|
||||
{
|
||||
var stream = GetStreamIfAvailable();
|
||||
try
|
||||
{
|
||||
var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr);
|
||||
var s7data = RequestTsdu(dataToSend);
|
||||
|
||||
stream.Write(dataToSend, 0, dataToSend.Length);
|
||||
|
||||
var s7data = COTP.TSDU.Read(stream);
|
||||
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
|
||||
}
|
||||
catch (Exception exc)
|
||||
@@ -442,10 +429,10 @@ namespace S7.Net
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads multiple vars in a single request.
|
||||
/// Reads multiple vars in a single request.
|
||||
/// You have to create and pass a list of DataItems and you obtain in response the same list with the values.
|
||||
/// Values are stored in the property "Value" of the dataItem and are already converted.
|
||||
/// If you don't want the conversion, just create a dataItem of bytes.
|
||||
/// If you don't want the conversion, just create a dataItem of bytes.
|
||||
/// The number of DataItems as well as the total size of the requested data can not exceed a certain limit (protocol restriction).
|
||||
/// </summary>
|
||||
/// <param name="dataItems">List of dataitems that contains the list of variables that must be read.</param>
|
||||
@@ -453,8 +440,6 @@ namespace S7.Net
|
||||
{
|
||||
AssertPduSizeForRead(dataItems);
|
||||
|
||||
var stream = GetStreamIfAvailable();
|
||||
|
||||
try
|
||||
{
|
||||
// first create the header
|
||||
@@ -468,9 +453,7 @@ namespace S7.Net
|
||||
}
|
||||
|
||||
var dataToSend = package.ToArray();
|
||||
stream.Write(dataToSend, 0, dataToSend.Length);
|
||||
|
||||
var s7data = COTP.TSDU.Read(stream); //TODO use Async
|
||||
var s7data = RequestTsdu(dataToSend);
|
||||
|
||||
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
|
||||
|
||||
@@ -481,5 +464,12 @@ namespace S7.Net
|
||||
throw new PlcException(ErrorCode.ReadData, exc);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] RequestTsdu(byte[] requestData) => RequestTsdu(requestData, 0, requestData.Length);
|
||||
|
||||
private byte[] RequestTsdu(byte[] requestData, int offset, int length)
|
||||
{
|
||||
return RequestTsduAsync(requestData, offset, length).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +1,27 @@
|
||||
using System;
|
||||
|
||||
namespace S7.Net.Protocol
|
||||
namespace S7.Net.Protocol
|
||||
{
|
||||
internal static class ConnectionRequest
|
||||
{
|
||||
public static byte[] GetCOTPConnectionRequest(CpuType cpu, Int16 rack, Int16 slot)
|
||||
public static byte[] GetCOTPConnectionRequest(TsapPair tsapPair)
|
||||
{
|
||||
byte[] bSend1 = {
|
||||
3, 0, 0, 22, //TPKT
|
||||
17, //COTP Header Length
|
||||
224, //Connect Request
|
||||
224, //Connect Request
|
||||
0, 0, //Destination Reference
|
||||
0, 46, //Source Reference
|
||||
0, //Flags
|
||||
193, //Parameter Code (src-tasp)
|
||||
2, //Parameter Length
|
||||
1, 0, //Source TASP
|
||||
tsapPair.Local.FirstByte, tsapPair.Local.SecondByte, //Source TASP
|
||||
194, //Parameter Code (dst-tasp)
|
||||
2, //Parameter Length
|
||||
3, 0, //Destination TASP
|
||||
tsapPair.Remote.FirstByte, tsapPair.Remote.SecondByte, //Destination TASP
|
||||
192, //Parameter Code (tpdu-size)
|
||||
1, //Parameter Length
|
||||
10 //TPDU Size (2^10 = 1024)
|
||||
};
|
||||
|
||||
switch (cpu)
|
||||
{
|
||||
case CpuType.S7200:
|
||||
//S7200: Chr(193) & Chr(2) & Chr(16) & Chr(0) 'Eigener Tsap
|
||||
bSend1[13] = 0x10;
|
||||
bSend1[14] = 0x00;
|
||||
//S7200: Chr(194) & Chr(2) & Chr(16) & Chr(0) 'Fremder Tsap
|
||||
bSend1[17] = 0x10;
|
||||
bSend1[18] = 0x00;
|
||||
break;
|
||||
case CpuType.Logo0BA8:
|
||||
// These values are taken from NodeS7, it's not verified if these are
|
||||
// exact requirements to connect to the Logo0BA8.
|
||||
bSend1[13] = 0x01;
|
||||
bSend1[14] = 0x00;
|
||||
bSend1[17] = 0x01;
|
||||
bSend1[18] = 0x02;
|
||||
break;
|
||||
case CpuType.S71200:
|
||||
case CpuType.S7300:
|
||||
case CpuType.S7400:
|
||||
//S7300: Chr(193) & Chr(2) & Chr(1) & Chr(0) 'Eigener Tsap
|
||||
bSend1[13] = 0x01;
|
||||
bSend1[14] = 0x00;
|
||||
//S7300: Chr(194) & Chr(2) & Chr(3) & Chr(2) 'Fremder Tsap
|
||||
bSend1[17] = 0x03;
|
||||
bSend1[18] = (byte) ((rack << 5) | (int) slot);
|
||||
break;
|
||||
case CpuType.S71500:
|
||||
// Eigener Tsap
|
||||
bSend1[13] = 0x10;
|
||||
bSend1[14] = 0x02;
|
||||
// Fredmer Tsap
|
||||
bSend1[17] = 0x03;
|
||||
bSend1[18] = (byte) ((rack << 5) | (int) slot);
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Wrong CPU Type Secified");
|
||||
}
|
||||
return bSend1;
|
||||
}
|
||||
}
|
||||
|
||||
31
S7.Net/Protocol/Tsap.cs
Normal file
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).");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net452;netstandard2.0;netstandard1.3</TargetFrameworks>
|
||||
<TargetFrameworks>net452;netstandard2.0;netstandard1.3;net5.0</TargetFrameworks>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile>
|
||||
<InternalsVisibleTo>S7.Net.UnitTest</InternalsVisibleTo>
|
||||
|
||||
@@ -25,34 +25,6 @@ namespace S7.Net
|
||||
Data = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a TPKT from the socket
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read from</param>
|
||||
/// <returns>TPKT Instance</returns>
|
||||
public static TPKT Read(Stream stream)
|
||||
{
|
||||
var buf = new byte[4];
|
||||
int len = stream.ReadExact(buf, 0, 4);
|
||||
if (len < 4) throw new TPKTInvalidException($"TPKT header is incomplete / invalid. Received Bytes: {len} expected: {buf.Length}");
|
||||
var version = buf[0];
|
||||
var reserved1 = buf[1];
|
||||
var length = buf[2] * 256 + buf[3]; //BigEndian
|
||||
|
||||
var data = new byte[length - 4];
|
||||
len = stream.ReadExact(data, 0, data.Length);
|
||||
if (len < data.Length)
|
||||
throw new TPKTInvalidException($"TPKT payload is incomplete / invalid. Received Bytes: {len} expected: {data.Length}");
|
||||
|
||||
return new TPKT
|
||||
(
|
||||
version: version,
|
||||
reserved1: reserved1,
|
||||
length: length,
|
||||
data: data
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a TPKT from the socket Async
|
||||
/// </summary>
|
||||
|
||||
@@ -14,20 +14,51 @@
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of a bit to 1 (true), given the address of the bit
|
||||
/// Sets the value of a bit to 1 (true), given the address of the bit. Returns
|
||||
/// a copy of the value with the bit set.
|
||||
/// </summary>
|
||||
/// <param name="value">The input value to modify.</param>
|
||||
/// <param name="bit">The index (zero based) of the bit to set.</param>
|
||||
/// <returns>The modified value with the bit at index set.</returns>
|
||||
public static byte SetBit(byte value, int bit)
|
||||
{
|
||||
return (byte)((value | (1 << bit)) & 0xFF);
|
||||
SetBit(ref value, bit);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of a bit to 1 (true), given the address of the bit.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to modify.</param>
|
||||
/// <param name="bit">The index (zero based) of the bit to set.</param>
|
||||
public static void SetBit(ref byte value, int bit)
|
||||
{
|
||||
value = (byte) ((value | (1 << bit)) & 0xFF);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the value of a bit to 0 (false), given the address of the bit. Returns
|
||||
/// a copy of the value with the bit cleared.
|
||||
/// </summary>
|
||||
/// <param name="value">The input value to modify.</param>
|
||||
/// <param name="bit">The index (zero based) of the bit to clear.</param>
|
||||
/// <returns>The modified value with the bit at index cleared.</returns>
|
||||
public static byte ClearBit(byte value, int bit)
|
||||
{
|
||||
ClearBit(ref value, bit);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the value of a bit to 0 (false), given the address of the bit
|
||||
/// </summary>
|
||||
public static byte ClearBit(byte value, int bit)
|
||||
/// <param name="value">The input value to modify.</param>
|
||||
/// <param name="bit">The index (zero based) of the bit to clear.</param>
|
||||
public static void ClearBit(ref byte value, int bit)
|
||||
{
|
||||
return (byte)((value | (~(1 << bit))) & 0xFF);
|
||||
value = (byte) (value & ~(1 << bit) & 0xFF);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace S7.Net.Types
|
||||
{
|
||||
@@ -25,7 +26,7 @@ namespace S7.Net.Types
|
||||
|
||||
}
|
||||
|
||||
private static double GetIncreasedNumberOfBytes(double numBytes, Type type)
|
||||
private static double GetIncreasedNumberOfBytes(double numBytes, Type type, PropertyInfo? propertyInfo)
|
||||
{
|
||||
switch (type.Name)
|
||||
{
|
||||
@@ -38,30 +39,30 @@ namespace S7.Net.Types
|
||||
break;
|
||||
case "Int16":
|
||||
case "UInt16":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
IncrementToEven(ref numBytes);
|
||||
numBytes += 2;
|
||||
break;
|
||||
case "Int32":
|
||||
case "UInt32":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
IncrementToEven(ref numBytes);
|
||||
numBytes += 4;
|
||||
break;
|
||||
case "Single":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
IncrementToEven(ref numBytes);
|
||||
numBytes += 4;
|
||||
break;
|
||||
case "Double":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
IncrementToEven(ref numBytes);
|
||||
numBytes += 8;
|
||||
break;
|
||||
case "String":
|
||||
S7StringAttribute? attribute = propertyInfo?.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
|
||||
if (attribute == default(S7StringAttribute))
|
||||
throw new ArgumentException("Please add S7StringAttribute to the string field");
|
||||
|
||||
IncrementToEven(ref numBytes);
|
||||
numBytes += attribute.ReservedLengthInBytes;
|
||||
break;
|
||||
default:
|
||||
var propertyClass = Activator.CreateInstance(type);
|
||||
numBytes = GetClassSize(propertyClass, numBytes, true);
|
||||
@@ -93,12 +94,12 @@ namespace S7.Net.Types
|
||||
IncrementToEven(ref numBytes);
|
||||
for (int i = 0; i < array.Length; i++)
|
||||
{
|
||||
numBytes = GetIncreasedNumberOfBytes(numBytes, elementType);
|
||||
numBytes = GetIncreasedNumberOfBytes(numBytes, elementType, property);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
numBytes = GetIncreasedNumberOfBytes(numBytes, property.PropertyType);
|
||||
numBytes = GetIncreasedNumberOfBytes(numBytes, property.PropertyType, property);
|
||||
}
|
||||
}
|
||||
if (false == isInnerProperty)
|
||||
@@ -111,7 +112,7 @@ namespace S7.Net.Types
|
||||
return numBytes;
|
||||
}
|
||||
|
||||
private static object? GetPropertyValue(Type propertyType, byte[] bytes, ref double numBytes)
|
||||
private static object? GetPropertyValue(Type propertyType, PropertyInfo? propertyInfo, byte[] bytes, ref double numBytes)
|
||||
{
|
||||
object? value = null;
|
||||
|
||||
@@ -133,50 +134,35 @@ namespace S7.Net.Types
|
||||
numBytes++;
|
||||
break;
|
||||
case "Int16":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
IncrementToEven(ref numBytes);
|
||||
// hier auswerten
|
||||
ushort source = Word.FromBytes(bytes[(int)numBytes + 1], bytes[(int)numBytes]);
|
||||
value = source.ConvertToShort();
|
||||
numBytes += 2;
|
||||
break;
|
||||
case "UInt16":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
IncrementToEven(ref numBytes);
|
||||
// hier auswerten
|
||||
value = Word.FromBytes(bytes[(int)numBytes + 1], bytes[(int)numBytes]);
|
||||
numBytes += 2;
|
||||
break;
|
||||
case "Int32":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
// hier auswerten
|
||||
uint sourceUInt = DWord.FromBytes(bytes[(int)numBytes + 3],
|
||||
bytes[(int)numBytes + 2],
|
||||
bytes[(int)numBytes + 1],
|
||||
bytes[(int)numBytes + 0]);
|
||||
IncrementToEven(ref numBytes);
|
||||
var wordBuffer = new byte[4];
|
||||
Array.Copy(bytes, (int)numBytes, wordBuffer, 0, wordBuffer.Length);
|
||||
uint sourceUInt = DWord.FromByteArray(wordBuffer);
|
||||
value = sourceUInt.ConvertToInt();
|
||||
numBytes += 4;
|
||||
break;
|
||||
case "UInt32":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
// hier auswerten
|
||||
value = DWord.FromBytes(
|
||||
bytes[(int)numBytes],
|
||||
bytes[(int)numBytes + 1],
|
||||
bytes[(int)numBytes + 2],
|
||||
bytes[(int)numBytes + 3]);
|
||||
IncrementToEven(ref numBytes);
|
||||
var wordBuffer2 = new byte[4];
|
||||
Array.Copy(bytes, (int)numBytes, wordBuffer2, 0, wordBuffer2.Length);
|
||||
value = DWord.FromByteArray(wordBuffer2);
|
||||
numBytes += 4;
|
||||
break;
|
||||
case "Single":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
IncrementToEven(ref numBytes);
|
||||
// hier auswerten
|
||||
value = Real.FromByteArray(
|
||||
new byte[] {
|
||||
@@ -187,15 +173,31 @@ namespace S7.Net.Types
|
||||
numBytes += 4;
|
||||
break;
|
||||
case "Double":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
IncrementToEven(ref numBytes);
|
||||
var buffer = new byte[8];
|
||||
Array.Copy(bytes, (int)numBytes, buffer, 0, 8);
|
||||
// hier auswerten
|
||||
value = LReal.FromByteArray(buffer);
|
||||
numBytes += 8;
|
||||
break;
|
||||
case "String":
|
||||
S7StringAttribute? attribute = propertyInfo?.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
|
||||
if (attribute == default(S7StringAttribute))
|
||||
throw new ArgumentException("Please add S7StringAttribute to the string field");
|
||||
|
||||
IncrementToEven(ref numBytes);
|
||||
|
||||
// get the value
|
||||
var sData = new byte[attribute.ReservedLengthInBytes];
|
||||
Array.Copy(bytes, (int)numBytes, sData, 0, sData.Length);
|
||||
value = attribute.Type switch
|
||||
{
|
||||
S7StringType.S7String => S7String.FromByteArray(sData),
|
||||
S7StringType.S7WString => S7WString.FromByteArray(sData),
|
||||
_ => throw new ArgumentException("Please use a valid string type for the S7StringAttribute")
|
||||
};
|
||||
numBytes += sData.Length;
|
||||
break;
|
||||
default:
|
||||
var propClass = Activator.CreateInstance(propertyType);
|
||||
numBytes = FromBytes(propClass, bytes, numBytes);
|
||||
@@ -227,7 +229,7 @@ namespace S7.Net.Types
|
||||
for (int i = 0; i < array.Length && numBytes < bytes.Length; i++)
|
||||
{
|
||||
array.SetValue(
|
||||
GetPropertyValue(elementType, bytes, ref numBytes),
|
||||
GetPropertyValue(elementType, property, bytes, ref numBytes),
|
||||
i);
|
||||
}
|
||||
}
|
||||
@@ -235,7 +237,7 @@ namespace S7.Net.Types
|
||||
{
|
||||
property.SetValue(
|
||||
sourceClass,
|
||||
GetPropertyValue(property.PropertyType, bytes, ref numBytes),
|
||||
GetPropertyValue(property.PropertyType, property, bytes, ref numBytes),
|
||||
null);
|
||||
}
|
||||
}
|
||||
@@ -243,7 +245,7 @@ namespace S7.Net.Types
|
||||
return numBytes;
|
||||
}
|
||||
|
||||
private static double SetBytesFromProperty(object propertyValue, byte[] bytes, double numBytes)
|
||||
private static double SetBytesFromProperty(object propertyValue, PropertyInfo? propertyInfo, byte[] bytes, double numBytes)
|
||||
{
|
||||
int bytePos = 0;
|
||||
int bitPos = 0;
|
||||
@@ -285,6 +287,18 @@ namespace S7.Net.Types
|
||||
case "Double":
|
||||
bytes2 = LReal.ToByteArray((double)propertyValue);
|
||||
break;
|
||||
case "String":
|
||||
S7StringAttribute? attribute = propertyInfo?.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
|
||||
if (attribute == default(S7StringAttribute))
|
||||
throw new ArgumentException("Please add S7StringAttribute to the string field");
|
||||
|
||||
bytes2 = attribute.Type switch
|
||||
{
|
||||
S7StringType.S7String => S7String.ToByteArray((string)propertyValue, attribute.ReservedLength),
|
||||
S7StringType.S7WString => S7WString.ToByteArray((string)propertyValue, attribute.ReservedLength),
|
||||
_ => throw new ArgumentException("Please use a valid string type for the S7StringAttribute")
|
||||
};
|
||||
break;
|
||||
default:
|
||||
numBytes = ToBytes(propertyValue, bytes, numBytes);
|
||||
break;
|
||||
@@ -320,12 +334,12 @@ namespace S7.Net.Types
|
||||
Type elementType = property.PropertyType.GetElementType();
|
||||
for (int i = 0; i < array.Length && numBytes < bytes.Length; i++)
|
||||
{
|
||||
numBytes = SetBytesFromProperty(array.GetValue(i), bytes, numBytes);
|
||||
numBytes = SetBytesFromProperty(array.GetValue(i), property, bytes, numBytes);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
numBytes = SetBytesFromProperty(property.GetValue(sourceClass, null), bytes, numBytes);
|
||||
numBytes = SetBytesFromProperty(property.GetValue(sourceClass, null), property, bytes, numBytes);
|
||||
}
|
||||
}
|
||||
return numBytes;
|
||||
|
||||
@@ -8,7 +8,19 @@ namespace S7.Net.Types
|
||||
/// An S7 String has a preceeding 2 byte header containing its capacity and length
|
||||
/// </summary>
|
||||
public static class S7String
|
||||
{
|
||||
{
|
||||
private static Encoding stringEncoding = Encoding.ASCII;
|
||||
|
||||
/// <summary>
|
||||
/// The Encoding used when serializing and deserializing S7String (Encoding.ASCII by default)
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentNullException">StringEncoding must not be null</exception>
|
||||
public static Encoding StringEncoding
|
||||
{
|
||||
get => stringEncoding;
|
||||
set => stringEncoding = value ?? throw new ArgumentNullException(nameof(StringEncoding));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts S7 bytes to a string
|
||||
/// </summary>
|
||||
@@ -30,7 +42,7 @@ namespace S7.Net.Types
|
||||
|
||||
try
|
||||
{
|
||||
return Encoding.ASCII.GetString(bytes, 2, length);
|
||||
return StringEncoding.GetString(bytes, 2, length);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -38,7 +50,6 @@ namespace S7.Net.Types
|
||||
$"Failed to parse {VarType.S7String} from data. Following fields were read: size: '{size}', actual length: '{length}', total number of bytes (including header): '{bytes.Length}'.",
|
||||
e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -56,7 +67,7 @@ namespace S7.Net.Types
|
||||
|
||||
if (reservedLength > 254) throw new ArgumentException($"The maximum string length supported is 254.");
|
||||
|
||||
var bytes = Encoding.ASCII.GetBytes(value);
|
||||
var bytes = StringEncoding.GetBytes(value);
|
||||
if (bytes.Length > reservedLength) throw new ArgumentException($"The provided string length ({bytes.Length} is larger than the specified reserved length ({reservedLength}).");
|
||||
|
||||
var buffer = new byte[2 + reservedLength];
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace S7.Net.Types
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
|
||||
public sealed class S7StringAttribute : Attribute
|
||||
{
|
||||
private readonly S7StringType type;
|
||||
|
||||
1
S7.sln
1
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}"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
image: Visual Studio 2019
|
||||
image: Visual Studio 2022
|
||||
configuration: Release
|
||||
install:
|
||||
- choco install gitversion.portable -y
|
||||
|
||||
Reference in New Issue
Block a user