Skip to content

Commit

Permalink
Release S7NetPlus 0.17.0
Browse files Browse the repository at this point in the history
Release highlights:
- Add Read-/WriteBytes overloads for Span<byte> and Memory<byte>
  • Loading branch information
mycroes committed May 30, 2023
2 parents 43b2982 + ab70bfb commit 2fc9eaa
Show file tree
Hide file tree
Showing 4 changed files with 391 additions and 1 deletion.
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

0 comments on commit 2fc9eaa

Please sign in to comment.