Merge branch 'develop' into githubActions2

This commit is contained in:
Serge Camille
2021-06-05 17:52:59 +02:00
10 changed files with 279 additions and 145 deletions

View File

@@ -933,7 +933,14 @@ namespace S7.Net.UnitTest
S7TestServer.Stop(); S7TestServer.Stop();
var unreachablePlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 2); 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] [TestMethod]
@@ -944,7 +951,8 @@ namespace S7.Net.UnitTest
S7TestServer.Start(TestServerPort); S7TestServer.Start(TestServerPort);
var reachablePlc = CreatePlc(); var reachablePlc = CreatePlc();
Assert.IsTrue(reachablePlc.IsAvailable); reachablePlc.Open();
Assert.IsTrue(reachablePlc.IsConnected);
} }
[TestMethod] [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

@@ -117,13 +117,24 @@ namespace S7.Net.UnitTest.TypeTests
AssertToByteArrayAndBackEquals("Abc", 4, 4, 3, (byte) 'A', (byte) 'b', (byte) 'c', 0); 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) private static void AssertFromByteArrayEquals(string expected, params byte[] bytes)
{ {
var convertedString = S7String.FromByteArray(bytes); var convertedString = S7String.FromByteArray(bytes);
Assert.AreEqual(expected, convertedString); Assert.AreEqual(expected, convertedString);
} }
private static void AssertToByteArrayAndBackEquals(string value, int reservedLength, params byte[] expected) private static void AssertToByteArrayAndBackEquals(string value, int reservedLength, params byte[] expected)
{ {
var convertedData = S7String.ToByteArray(value, reservedLength); var convertedData = S7String.ToByteArray(value, reservedLength);
@@ -131,5 +142,11 @@ namespace S7.Net.UnitTest.TypeTests
var convertedBack = S7String.FromByteArray(convertedData); var convertedBack = S7String.FromByteArray(convertedData);
Assert.AreEqual(value, convertedBack); 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

@@ -122,6 +122,17 @@ namespace S7.Net.UnitTest.TypeTests
Assert.AreEqual(expected, convertedString); 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) 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); var convertedBack = S7WString.FromByteArray(convertedData);
Assert.AreEqual(value, convertedBack); 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,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;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Net.Sockets; using System.Net.Sockets;
using S7.Net.Internal;
using S7.Net.Protocol; using S7.Net.Protocol;
using S7.Net.Types; using S7.Net.Types;
@@ -13,8 +15,10 @@ namespace S7.Net
/// </summary> /// </summary>
public partial class Plc : IDisposable public partial class Plc : IDisposable
{ {
private readonly TaskQueue queue = new TaskQueue();
private const int CONNECTION_TIMED_OUT_ERROR_CODE = 10060; private const int CONNECTION_TIMED_OUT_ERROR_CODE = 10060;
//TCP connection to device //TCP connection to device
private TcpClient? tcpClient; private TcpClient? tcpClient;
private NetworkStream? _stream; private NetworkStream? _stream;
@@ -75,47 +79,26 @@ namespace S7.Net
if (tcpClient != null) tcpClient.SendTimeout = writeTimeout; 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> /// <summary>
/// Checks if the socket is connected and polls the other peer (the PLC) to see if it's connected. /// Gets a value indicating whether a connection to the PLC has been established.
/// 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
/// </summary> /// </summary>
public bool IsConnected /// <remarks>
{ /// The <see cref="IsConnected"/> property gets the connection state of the Client socket as
get /// of the last I/O operation. When it returns <c>false</c>, the Client socket was either
{ /// never connected, or is no longer connected.
try ///
{ /// <para>
if (tcpClient == null) /// Because the <see cref="IsConnected"/> property only reflects the state of the connection
return false; /// 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
//TODO: Actually check communication by sending an empty TPDU /// returns <c>true</c>. Note that this behavior is by design. You cannot reliably test the
return tcpClient.Connected; /// 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
catch { return false; } /// gracefully handle failed transmissions.
} /// </para>
} /// </remarks>
public bool IsConnected => tcpClient?.Connected ?? false;
/// <summary> /// <summary>
/// Creates a PLC object with all the parameters needed for connections. /// 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 #region IDisposable Support
private bool disposedValue = false; // To detect redundant calls private bool disposedValue = false; // To detect redundant calls

View File

@@ -30,8 +30,8 @@ namespace S7.Net
} }
/// <summary> /// <summary>
/// Create the bytes-package to request data from the PLC. You have to specify the memory type (dataType), /// 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. /// the address of the memory, the address of the byte and the bytes count.
/// </summary> /// </summary>
/// <param name="dataType">MemoryType (DB, Timer, Counter, etc.)</param> /// <param name="dataType">MemoryType (DB, Timer, Counter, etc.)</param>
/// <param name="db">Address of the memory to be read</param> /// <param name="db">Address of the memory to be read</param>
@@ -184,13 +184,15 @@ namespace S7.Net
switch (varType) switch (varType)
{ {
case VarType.Bit: case VarType.Bit:
return varCount + 7 / 8; return (varCount + 7) / 8;
case VarType.Byte: case VarType.Byte:
return (varCount < 1) ? 1 : varCount; return (varCount < 1) ? 1 : varCount;
case VarType.String: case VarType.String:
return varCount; return varCount;
case VarType.S7String: 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.Word:
case VarType.Timer: case VarType.Timer:
case VarType.Int: case VarType.Int:

View File

@@ -1,11 +1,11 @@
using S7.Net.Types; using S7.Net.Types;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading.Tasks; using System.Threading.Tasks;
using S7.Net.Protocol; using S7.Net.Protocol;
using System.IO;
using System.Threading; using System.Threading;
using S7.Net.Protocol.S7; using S7.Net.Protocol.S7;
@@ -28,9 +28,14 @@ namespace S7.Net
var stream = await ConnectAsync().ConfigureAwait(false); var stream = await ConnectAsync().ConfigureAwait(false);
try try
{ {
cancellationToken.ThrowIfCancellationRequested(); await queue.Enqueue(async () =>
await EstablishConnection(stream, cancellationToken).ConfigureAwait(false); {
_stream = stream; cancellationToken.ThrowIfCancellationRequested();
await EstablishConnection(stream, cancellationToken).ConfigureAwait(false);
_stream = stream;
return default(object);
}).ConfigureAwait(false);
} }
catch(Exception) catch(Exception)
{ {
@@ -47,29 +52,30 @@ namespace S7.Net
return tcpClient.GetStream(); 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 RequestConnection(stream, cancellationToken).ConfigureAwait(false);
await SetupConnection(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); var requestData = ConnectionRequest.GetCOTPConnectionRequest(CPU, Rack, Slot);
await stream.WriteAsync(requestData, 0, requestData.Length).ConfigureAwait(false); var response = await NoLockRequestTpduAsync(stream, requestData, cancellationToken).ConfigureAwait(false);
var response = await COTP.TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
if (response.PDUType != COTP.PduType.ConnectionConfirmed) if (response.PDUType != COTP.PduType.ConnectionConfirmed)
{ {
throw new InvalidDataException("Connection request was denied", response.TPkt.Data, 1, 0x0d); 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(); 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) if (s7data.Length < 2)
throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup"); 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. //This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0.
var maxToRead = Math.Min(count, MaxPDUSize - 18); 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; count -= maxToRead;
index += maxToRead; index += maxToRead;
} }
@@ -112,7 +118,7 @@ namespace S7.Net
} }
/// <summary> /// <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). /// 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. /// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary> /// </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) 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); 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); return ParseBytes(varType, bytes, varCount, bitAdr);
} }
@@ -142,7 +148,7 @@ namespace S7.Net
public async Task<object?> ReadAsync(string variable, CancellationToken cancellationToken = default) public async Task<object?> ReadAsync(string variable, CancellationToken cancellationToken = default)
{ {
var adr = new PLCAddress(variable); 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> /// <summary>
@@ -158,7 +164,7 @@ namespace S7.Net
{ {
int numBytes = Types.Struct.GetStructSize(structType); int numBytes = Types.Struct.GetStructSize(structType);
// now read the package // 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 // and decode it
return Types.Struct.FromBytes(structType, resultBytes); 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> /// <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 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> /// <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. /// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
/// </summary> /// </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="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="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. /// <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 // 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 // and decode it
Class.FromBytes(sourceClass, resultBytes); Class.FromBytes(sourceClass, resultBytes);
@@ -205,7 +211,7 @@ namespace S7.Net
} }
/// <summary> /// <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 /// 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. /// type, the class needs a default constructor.
/// </summary> /// </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> /// <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 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> /// <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. /// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
/// </summary> /// </summary>
/// <typeparam name="T">The class that will be instantiated</typeparam> /// <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 public async Task<T?> ReadClassAsync<T>(Func<T> classFactory, int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : class
{ {
var instance = classFactory(); 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; int readBytes = res.Item1;
if (readBytes <= 0) if (readBytes <= 0)
{ {
@@ -245,10 +251,10 @@ namespace S7.Net
} }
/// <summary> /// <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. /// 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. /// 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). /// The number of DataItems as well as the total size of the requested data can not exceed a certain limit (protocol restriction).
/// </summary> /// </summary>
/// <param name="dataItems">List of dataitems that contains the list of variables that must be read.</param> /// <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> /// 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) 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. //replies with bigger PDU size in connection setup.
AssertPduSizeForRead(dataItems); AssertPduSizeForRead(dataItems);
var stream = GetStreamIfAvailable();
try try
{ {
var dataToSend = BuildReadRequestPackage(dataItems.Select(d => DataItem.GetDataItemAddress(d)).ToList()); 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]); ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
ParseDataIntoDataItems(s7data, dataItems); ParseDataIntoDataItems(s7data, dataItems);
@@ -306,7 +309,7 @@ namespace S7.Net
while (count > 0) while (count > 0)
{ {
var maxToWrite = (int)Math.Min(count, MaxPDUSize - 35); 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; count -= maxToWrite;
localIndex += maxToWrite; localIndex += maxToWrite;
} }
@@ -328,7 +331,7 @@ namespace S7.Net
if (bitAdr < 0 || bitAdr > 7) 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)); 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> /// <summary>
@@ -347,7 +350,7 @@ namespace S7.Net
if (value < 0 || value > 1) if (value < 0 || value > 1)
throw new ArgumentException("Value must be 0 or 1", nameof(value)); 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> /// <summary>
@@ -370,7 +373,7 @@ namespace S7.Net
//Must be writing a bit value as bitAdr is specified //Must be writing a bit value as bitAdr is specified
if (value is bool boolean) 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) 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", "Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid",
bitAdr), nameof(bitAdr)); 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 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> /// <summary>
@@ -399,7 +402,7 @@ namespace S7.Net
public async Task WriteAsync(string variable, object value, CancellationToken cancellationToken = default) public async Task WriteAsync(string variable, object value, CancellationToken cancellationToken = default)
{ {
var adr = new PLCAddress(variable); 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> /// <summary>
@@ -414,7 +417,7 @@ namespace S7.Net
public async Task WriteStructAsync(object structValue, int db, int startByteAdr = 0, CancellationToken cancellationToken = default) public async Task WriteStructAsync(object structValue, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
{ {
var bytes = Struct.ToBytes(structValue).ToList(); 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> /// <summary>
@@ -430,17 +433,14 @@ namespace S7.Net
{ {
byte[] bytes = new byte[(int)Class.GetClassSize(classValue)]; byte[] bytes = new byte[(int)Class.GetClassSize(classValue)];
Types.Class.ToBytes(classValue, bytes); 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) 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); AssertReadResponse(s7data, count);
Array.Copy(s7data, 18, buffer, offset, count); Array.Copy(s7data, 18, buffer, offset, count);
@@ -456,13 +456,11 @@ namespace S7.Net
{ {
AssertPduSizeForWrite(dataItems); AssertPduSizeForWrite(dataItems);
var stream = GetStreamIfAvailable();
var message = new ByteArray(); var message = new ByteArray();
var length = S7WriteMultiple.CreateRequest(message, dataItems); 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); S7WriteMultiple.ParseResponse(response, response.Length, dataItems);
} }
@@ -476,15 +474,11 @@ namespace S7.Net
/// <returns>A task that represents the asynchronous write operation.</returns> /// <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) private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count, CancellationToken cancellationToken)
{ {
try try
{ {
var stream = GetStreamIfAvailable();
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value, dataOffset, count); 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]); ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
} }
catch (OperationCanceledException) 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) private async Task WriteBitWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue, CancellationToken cancellationToken)
{ {
var stream = GetStreamIfAvailable();
try try
{ {
var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr); 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]); ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
} }
catch (OperationCanceledException) 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) var stream = GetStreamIfAvailable();
{
throw new PlcException(ErrorCode.ConnectionError, "Plc is not connected"); return queue.Enqueue(() =>
} NoLockRequestTsduAsync(stream, requestData, offset, length, cancellationToken));
return _stream; }
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;
using System.IO; using System.IO;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using S7.Net.Protocol; using S7.Net.Protocol;
using S7.Net.Helper; using S7.Net.Helper;
@@ -54,7 +52,7 @@ namespace S7.Net
} }
/// <summary> /// <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). /// 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. /// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary> /// </summary>
@@ -115,10 +113,10 @@ namespace S7.Net
/// <summary> /// <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. /// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
/// </summary> /// </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="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="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>The number of read bytes</returns> /// <returns>The number of read bytes</returns>
@@ -138,7 +136,7 @@ namespace S7.Net
} }
/// <summary> /// <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 /// 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. /// type, the class needs a default constructor.
/// </summary> /// </summary>
@@ -152,7 +150,7 @@ namespace S7.Net
} }
/// <summary> /// <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. /// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
/// </summary> /// </summary>
/// <typeparam name="T">The class that will be instantiated</typeparam> /// <typeparam name="T">The class that will be instantiated</typeparam>
@@ -186,7 +184,7 @@ namespace S7.Net
while (count > 0) while (count > 0)
{ {
//TODO: Figure out how to use MaxPDUSize here //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. //replies with bigger PDU size in connection setup.
var maxToWrite = Math.Min(count, MaxPDUSize - 28);//TODO tested only when the MaxPDUSize is 480 var maxToWrite = Math.Min(count, MaxPDUSize - 28);//TODO tested only when the MaxPDUSize is 480
WriteBytesWithASingleRequest(dataType, db, startByteAdr + localIndex, value, localIndex, maxToWrite); 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) private void ReadBytesWithSingleRequest(DataType dataType, int db, int startByteAdr, byte[] buffer, int offset, int count)
{ {
var stream = GetStreamIfAvailable();
try try
{ {
// first create the header // first create the header
@@ -309,9 +306,7 @@ namespace S7.Net
BuildReadDataRequestPackage(package, dataType, db, startByteAdr, count); BuildReadDataRequestPackage(package, dataType, db, startByteAdr, count);
var dataToSend = package.ToArray(); var dataToSend = package.ToArray();
stream.Write(dataToSend, 0, dataToSend.Length); var s7data = RequestTsdu(dataToSend);
var s7data = COTP.TSDU.Read(stream);
AssertReadResponse(s7data, count); AssertReadResponse(s7data, count);
Array.Copy(s7data, 18, buffer, offset, count); Array.Copy(s7data, 18, buffer, offset, count);
@@ -331,13 +326,11 @@ namespace S7.Net
{ {
AssertPduSizeForWrite(dataItems); AssertPduSizeForWrite(dataItems);
var stream = GetStreamIfAvailable();
var message = new ByteArray(); var message = new ByteArray();
var length = S7WriteMultiple.CreateRequest(message, dataItems); 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); S7WriteMultiple.ParseResponse(response, response.Length, dataItems);
} }
@@ -345,12 +338,9 @@ namespace S7.Net
{ {
try try
{ {
var stream = GetStreamIfAvailable();
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value, dataOffset, count); 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]); ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
} }
catch (Exception exc) catch (Exception exc)
@@ -425,14 +415,11 @@ namespace S7.Net
private void WriteBitWithASingleRequest(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue) private void WriteBitWithASingleRequest(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue)
{ {
var stream = GetStreamIfAvailable();
try try
{ {
var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr); 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]); ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
} }
catch (Exception exc) catch (Exception exc)
@@ -442,10 +429,10 @@ namespace S7.Net
} }
/// <summary> /// <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. /// 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. /// 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). /// The number of DataItems as well as the total size of the requested data can not exceed a certain limit (protocol restriction).
/// </summary> /// </summary>
/// <param name="dataItems">List of dataitems that contains the list of variables that must be read.</param> /// <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); AssertPduSizeForRead(dataItems);
var stream = GetStreamIfAvailable();
try try
{ {
// first create the header // first create the header
@@ -468,9 +453,7 @@ namespace S7.Net
} }
var dataToSend = package.ToArray(); var dataToSend = package.ToArray();
stream.Write(dataToSend, 0, dataToSend.Length); var s7data = RequestTsdu(dataToSend);
var s7data = COTP.TSDU.Read(stream); //TODO use Async
ValidateResponseCode((ReadWriteErrorCode)s7data[14]); ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
@@ -481,5 +464,12 @@ namespace S7.Net
throw new PlcException(ErrorCode.ReadData, exc); 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

@@ -14,20 +14,51 @@
} }
/// <summary> /// <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> /// </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) 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> /// <summary>
/// Resets the value of a bit to 0 (false), given the address of the bit /// Resets the value of a bit to 0 (false), given the address of the bit
/// </summary> /// </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);
} }
} }
} }