40 Commits

Author SHA1 Message Date
Michael Croes
5318f94dd7 Release S7NetPlus 0.12.0
Release highlights:
- Add synchronization to methods interacting with the PLC
2021-06-02 21:28:48 +02:00
Michael Croes
ea3beff481 Merge pull request #394 from mycroes/sync
Add synchronization
2021-06-02 21:25:51 +02:00
Michael Croes
f67b1e773f Add missing ConfigureAwait in OpenAsync 2021-06-01 20:59:20 +02:00
Michael Croes
e93a656312 Fix locking for OpenAsync 2021-06-01 20:28:34 +02:00
Michael Croes
8035f71a16 Apply synchronization to stream actions 2021-05-31 22:57:13 +02:00
Michael Croes
df4f258290 Move GetStreamIfAvailable to PLC.cs 2021-05-31 21:23:32 +02:00
Michael Croes
5636b93a53 PlcSynchronous: Remove unused usings 2021-05-31 21:17:42 +02:00
Michael Croes
8ed1b840bc PlcSynchronous: Clenaup trailing whitespace 2021-05-31 21:17:23 +02:00
Michael Croes
2afed88231 Consolidate sync stream calls 2021-05-31 21:17:03 +02:00
Michael Croes
1ded47971b Consolidate async stream calls 2021-05-31 21:07:16 +02:00
Michael Croes
ced10b4eca Release S7NetPlus 0.11.0
Release highlights:
- Fix for byte length calculation of bits
- Fix for Boolean.ClearBit
- Added Boolean.ClearBit and .SetBit by reference
2021-05-10 22:55:28 +02:00
Michael Croes
632e1c14ac Merge pull request #391 from mycroes/set-bit-clear-bit
SetBit and ClearBit tests, fixes
2021-05-10 21:24:43 +02:00
Michael Croes
aa50280233 Boolean: Add SetBit and ClearBit by reference 2021-05-10 21:14:43 +02:00
Michael Croes
3a794e8a46 Cleanup trailing whitespace in PLC and PLCHelpers 2021-05-10 21:05:02 +02:00
Michael Croes
0b8bd66bf7 Fix bitwise operator in Boolean.ClearBit
Fixes #249
2021-05-10 20:50:46 +02:00
Michael Croes
e66d21af05 Add tests for Boolean.SetBit and Boolean.ClearBit 2021-05-10 20:49:15 +02:00
Michael Croes
44ee651ac4 Merge pull request #390 from mycroes/byte-length-bits
Fix byte length calculation for VarType.Bit
2021-05-10 20:32:34 +02:00
Michael Croes
a1b4694ef6 Fix calculation of byte length for VarType.Bit
Fixes #389
2021-05-10 20:25:38 +02:00
Michael Croes
d10c15b80f Release S7NetPlus 0.10.0
Release highlights:
- Additional ConfigureAwait(false) calls used internally
- Fix for S7WString length
- Remove IsAvailable property
2021-03-29 22:01:14 +02:00
Michael Croes
5225c8bffd Merge pull request #311 from mycroes/is-available
Remove IsAvailable, document IsConnected
2021-03-29 21:51:34 +02:00
Michael Croes
aa03400350 Document IsConnected, shorten implementation 2021-03-29 21:43:24 +02:00
Michael Croes
2b4ec6d9dd Remove IsAvailable
Addresses #275, #252, #208, #154.
2021-03-29 21:43:24 +02:00
Michael Croes
54f3de6c9f Merge pull request #382 from mycroes/s7wstring-length
Fix problems with S7 string types (S7String and S7WString) and calculated byte length
2021-03-29 21:12:18 +02:00
diego
e6d14587d3 Added four new test methods to check ByteLength for S7 String types 2021-03-29 20:51:11 +02:00
diego
e63d92c61c Fix ByteLength calculation for S7Strings when reserved is odd 2021-03-29 20:51:11 +02:00
diego
b4b94e1777 Fix wrong calculation for S7WString byte length 2021-03-29 20:51:11 +02:00
Michael Croes
13c25fc20b Merge pull request #379 from scamille/fix-sync-class
Sprinkle more .ConfigureAwait(false) into async library.
2021-03-29 20:30:43 +02:00
Serge Camille
82e29837a2 Sprinkle more .ConfigureAwait(false) into async library.
This helps to avoid deadlocks when using synchronous API which references the async one in applications with a synchronization context, like a GUI.
Should fix #344
2021-03-16 08:08:39 +01:00
Michael Croes
1afb07774b Release S7NetPlus 0.9.0
Release highlights:
- Added WString support
- Added support for strings in structs
- Set type for 'Z' and 'C' to counter instead of timer
2021-03-13 13:38:38 +01:00
Michael Croes
051091919f Merge pull request #365 from MCPC10/StructStringSupport
Added support for string types in a struct
2021-03-13 13:31:51 +01:00
Michael Croes
478c1aed52 Merge branch 'develop' into StructStringSupport 2021-03-13 13:30:31 +01:00
Mike Cremer
924eb9c48f Minor changes + removed default length in S7StringAttribute 2021-03-13 12:00:32 +01:00
Michael Croes
eb8e188c86 Merge pull request #373 from scamille/patch-1
Fix PLC parse for type "Z"/"C"
2021-03-12 20:46:17 +01:00
Serge Camille
37384d2a92 Fix PLC parse for type "Z"/"C" 2021-03-12 08:17:37 +01:00
Mike Cremer
fdd4519f64 Minor changes 2021-01-29 21:13:38 +01:00
Michael Croes
52c60f6eaf Merge pull request #363 from MCPC10/WString
Added WString support + fixed max size in S7String
2021-01-27 18:55:12 +01:00
Mike Cremer
926d74f1d2 Fixed length check and corresponding message for S7String 2021-01-27 17:55:16 +01:00
Mike Cremer
9b89acfb91 Removed unnecessary bitwise and's 2021-01-25 21:42:08 +01:00
Mike Cremer
de0a9e64dc Added support for strings in Struct type class 2021-01-24 13:56:07 +01:00
Mike Cremer
dfcc4c7408 Added WString support 2021-01-24 12:16:32 +01:00
19 changed files with 673 additions and 176 deletions

View File

@@ -1,4 +1,5 @@

using S7.Net.Types;
namespace S7.Net.UnitTest.Helpers
{
public struct TestStruct
@@ -7,6 +8,7 @@ namespace S7.Net.UnitTest.Helpers
/// DB1.DBX0.0
/// </summary>
public bool BitVariable00;
public bool BitVariable01;
public bool BitVariable02;
public bool BitVariable03;
@@ -19,6 +21,7 @@ namespace S7.Net.UnitTest.Helpers
/// DB1.DBX1.0
/// </summary>
public bool BitVariable10;
public bool BitVariable11;
public bool BitVariable12;
public bool BitVariable13;
@@ -51,5 +54,17 @@ namespace S7.Net.UnitTest.Helpers
/// DB1.DBD16
/// </summary>
public ushort DWordVariable;
/// <summary>
/// DB1.DBX20.0
/// </summary>
[S7String(S7StringType.S7WString, 10)]
public string WStringVariable;
/// <summary>
/// DB1.DBX44.0
/// </summary>
[S7String(S7StringType.S7String, 10)]
public string StringVariable;
}
}

View File

@@ -211,7 +211,9 @@ namespace S7.Net.UnitTest
IntVariable = -15000,
LRealVariable = -154.789,
RealVariable = -154.789f,
DWordVariable = 850
DWordVariable = 850,
WStringVariable = "ÄÜÉÊéà",
StringVariable = "Hallo"
};
plc.WriteStruct(tc, DB2);
// Values that are read from a struct are stored in a new struct, returned by the funcion ReadStruct
@@ -223,6 +225,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>
@@ -739,7 +743,9 @@ namespace S7.Net.UnitTest
IntVariable = -15000,
LRealVariable = -154.789,
RealVariable = -154.789f,
DWordVariable = 850
DWordVariable = 850,
WStringVariable = "ÄÜÉÊéà",
StringVariable = "Hallo"
};
plc.WriteStruct(ts, DB2);
@@ -756,6 +762,8 @@ namespace S7.Net.UnitTest
Assert.AreEqual(ts2.LRealVariable, ts2Generic.LRealVariable);
Assert.AreEqual(ts2.RealVariable, ts2Generic.RealVariable);
Assert.AreEqual(ts2.DWordVariable, ts2Generic.DWordVariable);
Assert.AreEqual(ts2.WStringVariable, ts2Generic.WStringVariable);
Assert.AreEqual(ts2.StringVariable, ts2Generic.StringVariable);
}
[TestMethod]

View File

@@ -212,6 +212,9 @@ namespace S7.Net.UnitTest
tc.LRealVariable = -154.789;
tc.RealVariable = -154.789f;
tc.DWordVariable = 850;
tc.WStringVariable = "ÄÜÉÊéà";
tc.StringVariable = "Hallo";
plc.WriteStruct(tc, DB2);
// Values that are read from a struct are stored in a new struct, returned by the funcion ReadStruct
TestStruct tc2 = (TestStruct)plc.ReadStruct(typeof(TestStruct), DB2);
@@ -222,6 +225,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>
@@ -783,6 +788,8 @@ namespace S7.Net.UnitTest
ts.LRealVariable = -154.789;
ts.RealVariable = -154.789f;
ts.DWordVariable = 850;
ts.WStringVariable = "ÄÜÉÊéà";
ts.StringVariable = "Hallo";
plc.WriteStruct(ts, DB2);
@@ -797,6 +804,8 @@ namespace S7.Net.UnitTest
Assert.AreEqual(ts2.LRealVariable, ts2Generic.LRealVariable);
Assert.AreEqual(ts2.RealVariable, ts2Generic.RealVariable);
Assert.AreEqual(ts2.DWordVariable, ts2Generic.DWordVariable);
Assert.AreEqual(ts2.WStringVariable, ts2Generic.WStringVariable);
Assert.AreEqual(ts2.StringVariable, ts2Generic.StringVariable);
}
[TestMethod, ExpectedException(typeof(PlcException))]
@@ -924,7 +933,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]
@@ -935,7 +951,8 @@ namespace S7.Net.UnitTest
S7TestServer.Start(TestServerPort);
var reachablePlc = CreatePlc();
Assert.IsTrue(reachablePlc.IsAvailable);
reachablePlc.Open();
Assert.IsTrue(reachablePlc.IsConnected);
}
[TestMethod]

View File

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

View File

@@ -100,7 +100,7 @@ namespace S7.Net.UnitTest.TypeTests
}
[TestMethod]
public void WriteAbcWithStringLargetThanReservedLength()
public void WriteAbcWithStringLargerThanReservedLength()
{
Assert.ThrowsException<ArgumentException>(() => S7String.ToByteArray("Abc", 2));
}
@@ -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);
}
}
}

View File

@@ -0,0 +1,151 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using S7.Net.Types;
using System;
namespace S7.Net.UnitTest.TypeTests
{
[TestClass]
public class S7WStringTests
{
[TestMethod]
public void ReadEmptyStringWithZeroLength()
{
AssertFromByteArrayEquals("", 0, 0 , 0, 0);
}
[TestMethod]
public void ReadEmptyStringWithOneCharLength()
{
AssertFromByteArrayEquals("", 0, 1, 0, 0, 0, 0);
}
[TestMethod]
public void ReadEmptyStringWithOneCharGarbage()
{
AssertFromByteArrayEquals("", 0, 1, 0, 0, 0x00, 0x41);
}
[TestMethod]
public void ReadMalformedStringTooShort()
{
Assert.ThrowsException<PlcException>(() => AssertFromByteArrayEquals("", 0, 1));
}
[TestMethod]
public void ReadMalformedStringSizeLargerThanCapacity()
{
Assert.ThrowsException<PlcException>(() => S7WString.FromByteArray(new byte[] { 0, 3, 0, 5, 0, 0x00, 0x41, 0x00, 0x41, 0x00, 0x41}));
}
[TestMethod]
public void ReadMalformedStringCapacityTooLarge()
{
Assert.ThrowsException<ArgumentException>(() => AssertToByteArrayAndBackEquals("", 20000, 0));
}
[TestMethod]
public void ReadA()
{
AssertFromByteArrayEquals("A", 0, 1, 0, 1, 0x00, 0x41);
}
[TestMethod]
public void ReadAbc()
{
AssertFromByteArrayEquals("Abc", 0, 3, 0, 3, 0x00, 0x41, 0x00, 0x62, 0x00, 0x63);
}
[TestMethod]
public void WriteNullWithReservedLengthZero()
{
Assert.ThrowsException<ArgumentNullException>(() => AssertToByteArrayAndBackEquals(null, 0, 0, 0, 0, 0));
}
[TestMethod]
public void WriteEmptyStringWithReservedLengthZero()
{
AssertToByteArrayAndBackEquals("", 0, 0, 0, 0, 0);
}
[TestMethod]
public void WriteAWithReservedLengthZero()
{
AssertToByteArrayAndBackEquals("", 0, 0, 0, 0, 0);
}
[TestMethod]
public void WriteNullWithReservedLengthOne()
{
Assert.ThrowsException<ArgumentNullException>(() => AssertToByteArrayAndBackEquals(null, 1, 0, 1 , 0, 0));
}
[TestMethod]
public void WriteEmptyStringWithReservedLengthOne()
{
AssertToByteArrayAndBackEquals("", 1, 0, 1, 0, 0, 0, 0);
}
[TestMethod]
public void WriteAWithReservedLengthOne()
{
AssertToByteArrayAndBackEquals("A", 1, 0, 1, 0, 1, 0x00, 0x41);
}
[TestMethod]
public void WriteAWithReservedLengthTwo()
{
AssertToByteArrayAndBackEquals("A", 2, 0, 2, 0, 1, 0x00, 0x41, 0, 0);
}
[TestMethod]
public void WriteAbcWithStringLargerThanReservedLength()
{
Assert.ThrowsException<ArgumentException>(() => S7WString.ToByteArray("Abc", 2));
}
[TestMethod]
public void WriteAbcWithReservedLengthThree()
{
AssertToByteArrayAndBackEquals("Abc", 3, 0, 3, 0, 3, 0x00, 0x41, 0x00, 0x62, 0x00, 0x63);
}
[TestMethod]
public void WriteAbcWithReservedLengthFour()
{
AssertToByteArrayAndBackEquals("Abc", 4, 0, 4, 0, 3, 0x00, 0x41, 0x00, 0x62, 0x00, 0x63, 0 , 0);
}
private static void AssertFromByteArrayEquals(string expected, params byte[] bytes)
{
var convertedString = S7WString.FromByteArray(bytes);
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)
{
var convertedData = S7WString.ToByteArray(value, reservedLength);
CollectionAssert.AreEqual(expected, convertedData);
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);
}
}
}

View File

@@ -178,6 +178,11 @@
/// </summary>
S7String,
/// <summary>
/// S7 WString variable type (variable)
/// </summary>
S7WString,
/// <summary>
/// Timer variable type
/// </summary>

View File

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

View File

@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using S7.Net.Internal;
using S7.Net.Protocol;
using S7.Net.Types;
@@ -13,8 +15,10 @@ namespace S7.Net
/// </summary>
public partial class Plc : IDisposable
{
private readonly TaskQueue queue = new TaskQueue();
private const int CONNECTION_TIMED_OUT_ERROR_CODE = 10060;
//TCP connection to device
private TcpClient? tcpClient;
private NetworkStream? _stream;
@@ -75,47 +79,26 @@ 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;
//TODO: Actually check communication by sending an empty TPDU
return tcpClient.Connected;
}
catch { 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;
/// <summary>
/// Creates a PLC object with all the parameters needed for connections.
@@ -263,6 +246,16 @@ namespace S7.Net
}
}
private Stream GetStreamIfAvailable()
{
if (_stream == null)
{
throw new PlcException(ErrorCode.ConnectionError, "Plc is not connected");
}
return _stream;
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls

View File

@@ -183,7 +183,7 @@
case "Z":
case "C":
// Counter
dataType = DataType.Timer;
dataType = DataType.Counter;
dbNumber = 0;
address = int.Parse(input.Substring(1));
varType = VarType.Counter;
@@ -204,4 +204,4 @@
}
}
}
}
}

View File

@@ -30,8 +30,8 @@ namespace S7.Net
}
/// <summary>
/// Create the bytes-package to request data from the PLC. You have to specify the memory type (dataType),
/// the address of the memory, the address of the byte and the bytes count.
/// Create the bytes-package to request data from the PLC. You have to specify the memory type (dataType),
/// the address of the memory, the address of the byte and the bytes count.
/// </summary>
/// <param name="dataType">MemoryType (DB, Timer, Counter, etc.)</param>
/// <param name="db">Address of the memory to be read</param>
@@ -125,6 +125,8 @@ namespace S7.Net
return Types.String.FromByteArray(bytes);
case VarType.S7String:
return S7String.FromByteArray(bytes);
case VarType.S7WString:
return S7WString.FromByteArray(bytes);
case VarType.Timer:
if (varCount == 1)
@@ -182,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:

View File

@@ -1,11 +1,11 @@
using S7.Net.Types;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Threading.Tasks;
using S7.Net.Protocol;
using System.IO;
using System.Threading;
using S7.Net.Protocol.S7;
@@ -28,9 +28,14 @@ namespace S7.Net
var stream = await ConnectAsync().ConfigureAwait(false);
try
{
cancellationToken.ThrowIfCancellationRequested();
await EstablishConnection(stream, cancellationToken).ConfigureAwait(false);
_stream = stream;
await queue.Enqueue(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
await EstablishConnection(stream, cancellationToken).ConfigureAwait(false);
_stream = stream;
return default(object);
}).ConfigureAwait(false);
}
catch(Exception)
{
@@ -47,29 +52,30 @@ namespace S7.Net
return tcpClient.GetStream();
}
private async Task EstablishConnection(NetworkStream stream, CancellationToken cancellationToken)
private async Task EstablishConnection(Stream stream, CancellationToken cancellationToken)
{
await RequestConnection(stream, cancellationToken).ConfigureAwait(false);
await SetupConnection(stream, cancellationToken).ConfigureAwait(false);
}
private async Task RequestConnection(NetworkStream stream, CancellationToken cancellationToken)
private async Task RequestConnection(Stream stream, CancellationToken cancellationToken)
{
var requestData = ConnectionRequest.GetCOTPConnectionRequest(CPU, Rack, Slot);
await stream.WriteAsync(requestData, 0, requestData.Length).ConfigureAwait(false);
var response = await COTP.TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
var 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 +110,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 +118,7 @@ namespace S7.Net
}
/// <summary>
/// Read and decode a certain number of bytes of the "VarType" provided.
/// Read and decode a certain number of bytes of the "VarType" provided.
/// This can be used to read multiple consecutive variables of the same type (Word, DWord, Int, etc).
/// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary>
@@ -127,7 +133,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 +148,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 +164,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 +181,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 +203,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 +211,7 @@ namespace S7.Net
}
/// <summary>
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified. To instantiate the class defined by the generic
/// type, the class needs a default constructor.
/// </summary>
@@ -217,11 +223,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 +240,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 +251,10 @@ namespace S7.Net
}
/// <summary>
/// Reads multiple vars in a single request.
/// Reads multiple vars in a single request.
/// You have to create and pass a list of DataItems and you obtain in response the same list with the values.
/// Values are stored in the property "Value" of the dataItem and are already converted.
/// If you don't want the conversion, just create a dataItem of bytes.
/// If you don't want the conversion, just create a dataItem of bytes.
/// The number of DataItems as well as the total size of the requested data can not exceed a certain limit (protocol restriction).
/// </summary>
/// <param name="dataItems">List of dataitems that contains the list of variables that must be read.</param>
@@ -256,18 +262,15 @@ namespace S7.Net
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
public async Task<List<DataItem>> ReadMultipleVarsAsync(List<DataItem> dataItems, CancellationToken cancellationToken = default)
{
//Snap7 seems to choke on PDU sizes above 256 even if snap7
//Snap7 seems to choke on PDU sizes above 256 even if snap7
//replies with bigger PDU size in connection setup.
AssertPduSizeForRead(dataItems);
var stream = GetStreamIfAvailable();
try
{
var dataToSend = BuildReadRequestPackage(dataItems.Select(d => DataItem.GetDataItemAddress(d)).ToList());
await stream.WriteAsync(dataToSend, 0, dataToSend.Length);
var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
var s7data = await COTP.TSDU.ReadAsync(stream, cancellationToken);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
ParseDataIntoDataItems(s7data, dataItems);
@@ -306,7 +309,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 +331,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 +350,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 +373,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 +383,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 +402,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 +417,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 +433,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)});
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 +456,11 @@ namespace S7.Net
{
AssertPduSizeForWrite(dataItems);
var stream = GetStreamIfAvailable();
var message = new ByteArray();
var length = S7WriteMultiple.CreateRequest(message, dataItems);
await stream.WriteAsync(message.Array, 0, length).ConfigureAwait(false);
var response = await COTP.TSDU.ReadAsync(stream, CancellationToken.None).ConfigureAwait(false);
var response = await RequestTsduAsync(message.Array, 0, length).ConfigureAwait(false);
S7WriteMultiple.ParseResponse(response, response.Length, dataItems);
}
@@ -476,15 +474,11 @@ namespace S7.Net
/// <returns>A task that represents the asynchronous write operation.</returns>
private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count, CancellationToken cancellationToken)
{
try
{
var stream = GetStreamIfAvailable();
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value, dataOffset, count);
var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false);
await stream.WriteAsync(dataToSend, 0, dataToSend.Length, cancellationToken);
var s7data = await COTP.TSDU.ReadAsync(stream, cancellationToken);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (OperationCanceledException)
@@ -499,15 +493,11 @@ namespace S7.Net
private async Task WriteBitWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue, CancellationToken cancellationToken)
{
var stream = GetStreamIfAvailable();
try
{
var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr);
var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false);
await stream.WriteAsync(dataToSend, 0, dataToSend.Length);
var s7data = await COTP.TSDU.ReadAsync(stream, cancellationToken);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (OperationCanceledException)
@@ -520,13 +510,33 @@ 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)
{
throw new PlcException(ErrorCode.ConnectionError, "Plc is not connected");
}
return _stream;
var stream = GetStreamIfAvailable();
return queue.Enqueue(() =>
NoLockRequestTsduAsync(stream, requestData, offset, length, cancellationToken));
}
private static async Task<COTP.TPDU> NoLockRequestTpduAsync(Stream stream, byte[] requestData,
CancellationToken cancellationToken = default)
{
await stream.WriteAsync(requestData, 0, requestData.Length, cancellationToken).ConfigureAwait(false);
var response = await COTP.TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
return response;
}
private static async Task<byte[]> NoLockRequestTsduAsync(Stream stream, byte[] requestData, int offset, int length,
CancellationToken cancellationToken = default)
{
await stream.WriteAsync(requestData, offset, length, cancellationToken).ConfigureAwait(false);
var response = await COTP.TSDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
return response;
}
}
}

View File

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

View File

@@ -17,10 +17,14 @@ namespace S7.Net.Protocol
{
throw new Exception($"DataItem.Value is null, cannot serialize. StartAddr={dataItem.StartByteAdr} VarType={dataItem.VarType}");
}
if (dataItem.Value is string s)
return dataItem.VarType == VarType.S7String
? S7String.ToByteArray(s, dataItem.Count)
: Types.String.ToByteArray(s, dataItem.Count);
return dataItem.VarType switch
{
VarType.S7String => S7String.ToByteArray(s, dataItem.Count),
VarType.S7WString => S7WString.ToByteArray(s, dataItem.Count),
_ => Types.String.ToByteArray(s, dataItem.Count)
};
return SerializeValue(dataItem.Value);
}
@@ -46,7 +50,7 @@ namespace S7.Net.Protocol
case "Double":
return Types.LReal.ToByteArray((double)value);
case "DateTime":
return Types.DateTime.ToByteArray((System.DateTime) value);
return Types.DateTime.ToByteArray((System.DateTime)value);
case "Byte[]":
return (byte[])value;
case "Int16[]":
@@ -64,10 +68,10 @@ namespace S7.Net.Protocol
case "String":
// Hack: This is backwards compatible with the old code, but functionally it's broken
// if the consumer does not pay attention to string length.
var stringVal = (string) value;
var stringVal = (string)value;
return Types.String.ToByteArray(stringVal, stringVal.Length);
case "DateTime[]":
return Types.DateTime.ToByteArray((System.DateTime[]) value);
return Types.DateTime.ToByteArray((System.DateTime[])value);
case "DateTimeLong[]":
return Types.DateTimeLong.ToByteArray((System.DateTime[])value);
default:

View File

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

View File

@@ -45,7 +45,7 @@ namespace S7.Net.Types
/// Converts a <see cref="T:string"/> to S7 string with 2-byte header.
/// </summary>
/// <param name="value">The string to convert to byte array.</param>
/// <param name="reservedLength">The length (in bytes) allocated in PLC for string excluding header.</param>
/// <param name="reservedLength">The length (in characters) allocated in PLC for the string.</param>
/// <returns>A <see cref="T:byte[]" /> containing the string header and string value with a maximum length of <paramref name="reservedLength"/> + 2.</returns>
public static byte[] ToByteArray(string value, int reservedLength)
{
@@ -54,7 +54,7 @@ namespace S7.Net.Types
throw new ArgumentNullException(nameof(value));
}
if (reservedLength > byte.MaxValue) throw new ArgumentException($"The maximum string length supported is {byte.MaxValue}.");
if (reservedLength > 254) throw new ArgumentException($"The maximum string length supported is 254.");
var bytes = Encoding.ASCII.GetBytes(value);
if (bytes.Length > reservedLength) throw new ArgumentException($"The provided string length ({bytes.Length} is larger than the specified reserved length ({reservedLength}).");

View File

@@ -0,0 +1,67 @@
using System;
namespace S7.Net.Types
{
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public sealed class S7StringAttribute : Attribute
{
private readonly S7StringType type;
private readonly int reservedLength;
/// <summary>
/// Initializes a new instance of the <see cref="S7StringAttribute"/> class.
/// </summary>
/// <param name="type">The string type.</param>
/// <param name="reservedLength">Reserved length of the string in characters.</param>
/// <exception cref="ArgumentException">Please use a valid value for the string type</exception>
public S7StringAttribute(S7StringType type, int reservedLength)
{
if (!Enum.IsDefined(typeof(S7StringType), type))
throw new ArgumentException("Please use a valid value for the string type");
this.type = type;
this.reservedLength = reservedLength;
}
/// <summary>
/// Gets the type of the string.
/// </summary>
/// <value>
/// The string type.
/// </value>
public S7StringType Type => type;
/// <summary>
/// Gets the reserved length of the string in characters.
/// </summary>
/// <value>
/// The reserved length of the string in characters.
/// </value>
public int ReservedLength => reservedLength;
/// <summary>
/// Gets the reserved length in bytes.
/// </summary>
/// <value>
/// The reserved length in bytes.
/// </value>
public int ReservedLengthInBytes => type == S7StringType.S7String ? reservedLength + 2 : (reservedLength * 2) + 4;
}
/// <summary>
/// String type.
/// </summary>
public enum S7StringType
{
/// <summary>
/// ASCII string.
/// </summary>
S7String = VarType.S7String,
/// <summary>
/// Unicode string.
/// </summary>
S7WString = VarType.S7WString
}
}

72
S7.Net/Types/S7WString.cs Normal file
View File

@@ -0,0 +1,72 @@
using System;
using System.Text;
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to convert from S7 wstrings to C# strings
/// An S7 WString has a preceding 4 byte header containing its capacity and length
/// </summary>
public static class S7WString
{
/// <summary>
/// Converts S7 bytes to a string
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static string FromByteArray(byte[] bytes)
{
if (bytes.Length < 4)
{
throw new PlcException(ErrorCode.ReadData, "Malformed S7 WString / too short");
}
int size = (bytes[0] << 8) | bytes[1];
int length = (bytes[2] << 8) | bytes[3];
if (length > size)
{
throw new PlcException(ErrorCode.ReadData, "Malformed S7 WString / length larger than capacity");
}
try
{
return Encoding.BigEndianUnicode.GetString(bytes, 4, length * 2);
}
catch (Exception e)
{
throw new PlcException(ErrorCode.ReadData,
$"Failed to parse {VarType.S7WString} from data. Following fields were read: size: '{size}', actual length: '{length}', total number of bytes (including header): '{bytes.Length}'.",
e);
}
}
/// <summary>
/// Converts a <see cref="T:string"/> to S7 wstring with 4-byte header.
/// </summary>
/// <param name="value">The string to convert to byte array.</param>
/// <param name="reservedLength">The length (in characters) allocated in PLC for the string.</param>
/// <returns>A <see cref="T:byte[]" /> containing the string header and string value with a maximum length of <paramref name="reservedLength"/> + 4.</returns>
public static byte[] ToByteArray(string value, int reservedLength)
{
if (value is null)
{
throw new ArgumentNullException(nameof(value));
}
if (reservedLength > 16382) throw new ArgumentException("The maximum string length supported is 16382.");
var buffer = new byte[4 + reservedLength * 2];
buffer[0] = (byte)((reservedLength >> 8) & 0xFF);
buffer[1] = (byte)(reservedLength & 0xFF);
buffer[2] = (byte)((value.Length >> 8) & 0xFF);
buffer[3] = (byte)(value.Length & 0xFF);
var stringLength = Encoding.BigEndianUnicode.GetBytes(value, 0, value.Length, buffer, 4) / 2;
if (stringLength > reservedLength) throw new ArgumentException($"The provided string length ({stringLength} is larger than the specified reserved length ({reservedLength}).");
return buffer;
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Reflection;
namespace S7.Net.Types
@@ -18,11 +19,11 @@ namespace S7.Net.Types
double numBytes = 0.0;
var infos = structType
#if NETSTANDARD1_3
#if NETSTANDARD1_3
.GetTypeInfo().DeclaredFields;
#else
#else
.GetFields();
#endif
#endif
foreach (var info in infos)
{
@@ -61,6 +62,16 @@ namespace S7.Net.Types
numBytes++;
numBytes += 8;
break;
case "String":
S7StringAttribute? attribute = info.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
if (attribute == default(S7StringAttribute))
throw new ArgumentException("Please add S7StringAttribute to the string field");
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
numBytes += attribute.ReservedLengthInBytes;
break;
default:
numBytes += GetStructSize(info.FieldType);
break;
@@ -91,11 +102,11 @@ namespace S7.Net.Types
var infos = structValue.GetType()
#if NETSTANDARD1_3
#if NETSTANDARD1_3
.GetTypeInfo().DeclaredFields;
#else
#else
.GetFields();
#endif
#endif
foreach (var info in infos)
{
@@ -120,7 +131,7 @@ namespace S7.Net.Types
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
// hier auswerten
// get the value
ushort source = Word.FromBytes(bytes[(int)numBytes + 1], bytes[(int)numBytes]);
info.SetValue(structValue, source.ConvertToShort());
numBytes += 2;
@@ -129,7 +140,7 @@ namespace S7.Net.Types
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
// hier auswerten
// get the value
info.SetValue(structValue, Word.FromBytes(bytes[(int)numBytes + 1],
bytes[(int)numBytes]));
numBytes += 2;
@@ -138,7 +149,7 @@ namespace S7.Net.Types
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
// hier auswerten
// get the value
uint sourceUInt = DWord.FromBytes(bytes[(int)numBytes + 3],
bytes[(int)numBytes + 2],
bytes[(int)numBytes + 1],
@@ -150,7 +161,7 @@ namespace S7.Net.Types
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
// hier auswerten
// get the value
info.SetValue(structValue, DWord.FromBytes(bytes[(int)numBytes],
bytes[(int)numBytes + 1],
bytes[(int)numBytes + 2],
@@ -161,7 +172,7 @@ namespace S7.Net.Types
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
// hier auswerten
// get the value
info.SetValue(structValue, Real.FromByteArray(new byte[] { bytes[(int)numBytes],
bytes[(int)numBytes + 1],
bytes[(int)numBytes + 2],
@@ -172,12 +183,38 @@ namespace S7.Net.Types
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
// hier auswerten
// get the value
var data = new byte[8];
Array.Copy(bytes, (int)numBytes, data, 0, 8);
info.SetValue(structValue, LReal.FromByteArray(data));
numBytes += 8;
break;
case "String":
S7StringAttribute? attribute = info.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
if (attribute == default(S7StringAttribute))
throw new ArgumentException("Please add S7StringAttribute to the string field");
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
// get the value
var sData = new byte[attribute.ReservedLengthInBytes];
Array.Copy(bytes, (int)numBytes, sData, 0, sData.Length);
switch (attribute.Type)
{
case S7StringType.S7String:
info.SetValue(structValue, S7String.FromByteArray(sData));
break;
case S7StringType.S7WString:
info.SetValue(structValue, S7WString.FromByteArray(sData));
break;
default:
throw new ArgumentException("Please use a valid string type for the S7StringAttribute");
}
numBytes += sData.Length;
break;
default:
var buffer = new byte[GetStructSize(info.FieldType)];
if (buffer.Length == 0)
@@ -209,11 +246,11 @@ namespace S7.Net.Types
double numBytes = 0.0;
var infos = type
#if NETSTANDARD1_3
#if NETSTANDARD1_3
.GetTypeInfo().DeclaredFields;
#else
#else
.GetFields();
#endif
#endif
foreach (var info in infos)
{
@@ -254,6 +291,18 @@ namespace S7.Net.Types
case "Double":
bytes2 = LReal.ToByteArray((double)info.GetValue(structValue));
break;
case "String":
S7StringAttribute? attribute = info.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)info.GetValue(structValue), attribute.ReservedLength),
S7StringType.S7WString => S7WString.ToByteArray((string)info.GetValue(structValue), attribute.ReservedLength),
_ => throw new ArgumentException("Please use a valid string type for the S7StringAttribute")
};
break;
}
if (bytes2 != null)
{
@@ -269,7 +318,5 @@ namespace S7.Net.Types
}
return bytes;
}
}
}