Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions S7.Net.UnitTest/S7NetTestsAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
using System.Threading;
using System.Security.Cryptography;


#if NET5_0_OR_GREATER
using System.Buffers;
#endif

#endregion

/**
Expand Down Expand Up @@ -139,6 +144,33 @@ public async Task Test_Async_WriteLargeByteArray()
CollectionAssert.AreEqual(data, readData);
}

#if NET5_0_OR_GREATER

/// <summary>
/// Write/Read a large amount of data to test PDU max
/// </summary>
[TestMethod]
public async Task Test_Async_WriteLargeByteArrayWithMemory()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");

var randomEngine = new Random();
using var dataOwner = MemoryPool<byte>.Shared.Rent(8192);
var data = dataOwner.Memory.Slice(0, 8192);
var db = 2;
randomEngine.NextBytes(data.Span);

await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data);

using var readDataOwner = MemoryPool<byte>.Shared.Rent(data.Length);
var readData = readDataOwner.Memory.Slice(0, data.Length);
await plc.ReadBytesAsync(readData, DataType.DataBlock, db, 0);

CollectionAssert.AreEqual(data.ToArray(), readData.ToArray());
}

#endif

/// <summary>
/// Read/Write a class that has the same properties of a DB with the same field in the same order
/// </summary>
Expand Down Expand Up @@ -933,6 +965,31 @@ public async Task Test_Async_ReadWriteBytesMany()
}
}

#if NET5_0_OR_GREATER

[TestMethod]
public async Task Test_Async_ReadWriteBytesManyWithMemory()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");

using var data = MemoryPool<byte>.Shared.Rent(2000);
for (int i = 0; i < data.Memory.Length; i++)
data.Memory.Span[i] = (byte)(i % 256);

await plc.WriteBytesAsync(DataType.DataBlock, 2, 0, data.Memory);

using var readData = MemoryPool<byte>.Shared.Rent(data.Memory.Length);

await plc.ReadBytesAsync(readData.Memory.Slice(0, data.Memory.Length), DataType.DataBlock, 2, 0);

for (int x = 0; x < data.Memory.Length; x++)
{
Assert.AreEqual(x % 256, readData.Memory.Span[x], string.Format("Bit {0} failed", x));
}
}

#endif

/// <summary>
/// Write a large amount of data and test cancellation
/// </summary>
Expand Down Expand Up @@ -969,6 +1026,47 @@ public async Task Test_Async_WriteLargeByteArrayWithCancellation()
Console.WriteLine("Task was not cancelled as expected.");
}

#if NET5_0_OR_GREATER

/// <summary>
/// Write a large amount of data and test cancellation
/// </summary>
[TestMethod]
public async Task Test_Async_WriteLargeByteArrayWithCancellationWithMemory()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");

var cancellationSource = new CancellationTokenSource();
var cancellationToken = cancellationSource.Token;

using var dataOwner = MemoryPool<byte>.Shared.Rent(8192);
var data = dataOwner.Memory.Slice(0, 8192);
var randomEngine = new Random();
var db = 2;
randomEngine.NextBytes(data.Span);

cancellationSource.CancelAfter(TimeSpan.FromMilliseconds(5));
try
{
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data, cancellationToken);
}
catch (OperationCanceledException)
{
// everything is good, that is the exception we expect
Console.WriteLine("Operation was cancelled as expected.");
return;
}
catch (Exception e)
{
Assert.Fail($"Wrong exception type received. Expected {typeof(OperationCanceledException)}, received {e.GetType()}.");
}

// Depending on how tests run, this can also just succeed without getting cancelled at all. Do nothing in this case.
Console.WriteLine("Task was not cancelled as expected.");
}

#endif

/// <summary>
/// Write a large amount of data and test cancellation
/// </summary>
Expand Down Expand Up @@ -1001,6 +1099,7 @@ public async Task Test_Async_ParseDataIntoDataItemsAlignment()
};
await plc.ReadMultipleVarsAsync(dataItems, CancellationToken.None);
}

#endregion
}
}
59 changes: 58 additions & 1 deletion S7.Net.UnitTest/S7NetTestsSync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
using S7.UnitTest.Helpers;
using System.Security.Cryptography;

#if NET5_0_OR_GREATER
using System.Buffers;
#endif

#endregion

/**
Expand Down Expand Up @@ -778,6 +782,33 @@ public void T33_WriteLargeByteArray()
CollectionAssert.AreEqual(data, readData);
}

#if NET5_0_OR_GREATER

/// <summary>
/// Write/Read a large amount of data to test PDU max
/// </summary>
[TestMethod]
public void T33_WriteLargeByteArrayWithSpan()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");

var randomEngine = new Random();
using var dataOwner = MemoryPool<byte>.Shared.Rent(8192);
var data = dataOwner.Memory.Span.Slice(0, 8192);
var db = 2;
randomEngine.NextBytes(data);

plc.WriteBytes(DataType.DataBlock, db, 0, data);

using var readDataOwner = MemoryPool<byte>.Shared.Rent(data.Length);
var readData = readDataOwner.Memory.Span.Slice(0, data.Length);
plc.ReadBytes(readData, DataType.DataBlock, db, 0);

CollectionAssert.AreEqual(data.ToArray(), readData.ToArray());
}

#endif

[TestMethod, ExpectedException(typeof(PlcException))]
public void T18_ReadStructThrowsIfPlcIsNotConnected()
{
Expand Down Expand Up @@ -1006,6 +1037,32 @@ public void T27_ReadWriteBytesMany()
}
}

#if NET5_0_OR_GREATER

[TestMethod]
public void T27_ReadWriteBytesManyWithSpan()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");

using var dataOwner = MemoryPool<byte>.Shared.Rent(2000);
var data = dataOwner.Memory.Span;
for (int i = 0; i < data.Length; i++)
data[i] = (byte)(i % 256);

plc.WriteBytes(DataType.DataBlock, 2, 0, data);

using var readDataOwner = MemoryPool<byte>.Shared.Rent(data.Length);
var readData = readDataOwner.Memory.Span.Slice(0, data.Length);
plc.ReadBytes(readData, DataType.DataBlock, 2, 0);

for (int x = 0; x < data.Length; x++)
{
Assert.AreEqual(x % 256, readData[x], $"Mismatch at offset {x}, expected {x % 256}, actual {readData[x]}.");
}
}

#endif

[TestMethod]
public void T28_ReadClass_DoesntCrash_When_ReadingLessThan1Byte()
{
Expand Down Expand Up @@ -1060,7 +1117,7 @@ public void T33_ReadWriteDateTimeLong()
Assert.AreEqual(test_value, test_value2, "Compare DateTimeLong Write/Read");
}

#endregion
#endregion

#region Private methods

Expand Down
100 changes: 100 additions & 0 deletions S7.Net/PlcAsynchronous.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,34 @@ public async Task<byte[]> ReadBytesAsync(DataType dataType, int db, int startByt
return resultBytes;
}

#if NET5_0_OR_GREATER

/// <summary>
/// Reads a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
/// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="buffer">Buffer to receive the read bytes. The <see cref="Memory{T}.Length"/> determines the number of bytes to read.</param>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</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.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>Returns the bytes in an array</returns>
public async Task ReadBytesAsync(Memory<byte> buffer, DataType dataType, int db, int startByteAdr, CancellationToken cancellationToken = default)
{
int index = 0;
while (buffer.Length > 0)
{
//This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0.
var maxToRead = Math.Min(buffer.Length, MaxPDUSize - 18);
await ReadBytesWithSingleRequestAsync(dataType, db, startByteAdr + index, buffer.Slice(0, maxToRead), cancellationToken).ConfigureAwait(false);
buffer = buffer.Slice(maxToRead);
index += maxToRead;
}
}

#endif

/// <summary>
/// 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).
Expand Down Expand Up @@ -320,6 +348,33 @@ public async Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, b
}
}

#if NET5_0_OR_GREATER

/// <summary>
/// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
/// If the write was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, ReadOnlyMemory<byte> value, CancellationToken cancellationToken = default)
{
int localIndex = 0;
while (value.Length > 0)
{
var maxToWrite = (int)Math.Min(value.Length, MaxPDUSize - 35);
await WriteBytesWithASingleRequestAsync(dataType, db, startByteAdr + localIndex, value.Slice(0, maxToWrite), cancellationToken).ConfigureAwait(false);
value = value.Slice(maxToWrite);
localIndex += maxToWrite;
}
}

#endif

/// <summary>
/// Write a single bit from a DB with the specified index.
/// </summary>
Expand Down Expand Up @@ -451,6 +506,20 @@ private async Task ReadBytesWithSingleRequestAsync(DataType dataType, int db, in
Array.Copy(s7data, 18, buffer, offset, count);
}

#if NET5_0_OR_GREATER

private async Task ReadBytesWithSingleRequestAsync(DataType dataType, int db, int startByteAdr, Memory<byte> buffer, CancellationToken cancellationToken)
{
var dataToSend = BuildReadRequestPackage(new[] { new DataItemAddress(dataType, db, startByteAdr, buffer.Length) });

var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
AssertReadResponse(s7data, buffer.Length);

s7data.AsSpan(18, buffer.Length).CopyTo(buffer.Span);
}

#endif

/// <summary>
/// Write DataItem(s) to the PLC. Throws an exception if the response is invalid
/// or when the PLC reports errors for item(s) written.
Expand Down Expand Up @@ -496,6 +565,37 @@ private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db,
}
}

#if NET5_0_OR_GREATER

/// <summary>
/// Writes up to 200 bytes to the PLC. You must specify the memory area type, memory are address, byte start address and bytes count.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, ReadOnlyMemory<byte> value, CancellationToken cancellationToken)
{
try
{
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value.Span);
var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false);

ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.WriteData, exc);
}
}

#endif

private async Task WriteBitWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue, CancellationToken cancellationToken)
{
try
Expand Down
Loading