Skip to content

Commit ab70bfb

Browse files
authored
Merge pull request #482 from ArgusMagnus/add_span_overloads
add Read/WriteBytes(Async) overloads accepting Span<byte>/Memory<byte> for .NET5 or greater
2 parents 6aa0133 + e277cf6 commit ab70bfb

File tree

4 files changed

+391
-1
lines changed

4 files changed

+391
-1
lines changed

S7.Net.UnitTest/S7NetTestsAsync.cs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
using System.Threading;
1010
using System.Security.Cryptography;
1111

12+
13+
#if NET5_0_OR_GREATER
14+
using System.Buffers;
15+
#endif
16+
1217
#endregion
1318

1419
/**
@@ -139,6 +144,33 @@ public async Task Test_Async_WriteLargeByteArray()
139144
CollectionAssert.AreEqual(data, readData);
140145
}
141146

147+
#if NET5_0_OR_GREATER
148+
149+
/// <summary>
150+
/// Write/Read a large amount of data to test PDU max
151+
/// </summary>
152+
[TestMethod]
153+
public async Task Test_Async_WriteLargeByteArrayWithMemory()
154+
{
155+
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
156+
157+
var randomEngine = new Random();
158+
using var dataOwner = MemoryPool<byte>.Shared.Rent(8192);
159+
var data = dataOwner.Memory.Slice(0, 8192);
160+
var db = 2;
161+
randomEngine.NextBytes(data.Span);
162+
163+
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data);
164+
165+
using var readDataOwner = MemoryPool<byte>.Shared.Rent(data.Length);
166+
var readData = readDataOwner.Memory.Slice(0, data.Length);
167+
await plc.ReadBytesAsync(readData, DataType.DataBlock, db, 0);
168+
169+
CollectionAssert.AreEqual(data.ToArray(), readData.ToArray());
170+
}
171+
172+
#endif
173+
142174
/// <summary>
143175
/// Read/Write a class that has the same properties of a DB with the same field in the same order
144176
/// </summary>
@@ -933,6 +965,31 @@ public async Task Test_Async_ReadWriteBytesMany()
933965
}
934966
}
935967

968+
#if NET5_0_OR_GREATER
969+
970+
[TestMethod]
971+
public async Task Test_Async_ReadWriteBytesManyWithMemory()
972+
{
973+
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
974+
975+
using var data = MemoryPool<byte>.Shared.Rent(2000);
976+
for (int i = 0; i < data.Memory.Length; i++)
977+
data.Memory.Span[i] = (byte)(i % 256);
978+
979+
await plc.WriteBytesAsync(DataType.DataBlock, 2, 0, data.Memory);
980+
981+
using var readData = MemoryPool<byte>.Shared.Rent(data.Memory.Length);
982+
983+
await plc.ReadBytesAsync(readData.Memory.Slice(0, data.Memory.Length), DataType.DataBlock, 2, 0);
984+
985+
for (int x = 0; x < data.Memory.Length; x++)
986+
{
987+
Assert.AreEqual(x % 256, readData.Memory.Span[x], string.Format("Bit {0} failed", x));
988+
}
989+
}
990+
991+
#endif
992+
936993
/// <summary>
937994
/// Write a large amount of data and test cancellation
938995
/// </summary>
@@ -969,6 +1026,47 @@ public async Task Test_Async_WriteLargeByteArrayWithCancellation()
9691026
Console.WriteLine("Task was not cancelled as expected.");
9701027
}
9711028

1029+
#if NET5_0_OR_GREATER
1030+
1031+
/// <summary>
1032+
/// Write a large amount of data and test cancellation
1033+
/// </summary>
1034+
[TestMethod]
1035+
public async Task Test_Async_WriteLargeByteArrayWithCancellationWithMemory()
1036+
{
1037+
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
1038+
1039+
var cancellationSource = new CancellationTokenSource();
1040+
var cancellationToken = cancellationSource.Token;
1041+
1042+
using var dataOwner = MemoryPool<byte>.Shared.Rent(8192);
1043+
var data = dataOwner.Memory.Slice(0, 8192);
1044+
var randomEngine = new Random();
1045+
var db = 2;
1046+
randomEngine.NextBytes(data.Span);
1047+
1048+
cancellationSource.CancelAfter(TimeSpan.FromMilliseconds(5));
1049+
try
1050+
{
1051+
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data, cancellationToken);
1052+
}
1053+
catch (OperationCanceledException)
1054+
{
1055+
// everything is good, that is the exception we expect
1056+
Console.WriteLine("Operation was cancelled as expected.");
1057+
return;
1058+
}
1059+
catch (Exception e)
1060+
{
1061+
Assert.Fail($"Wrong exception type received. Expected {typeof(OperationCanceledException)}, received {e.GetType()}.");
1062+
}
1063+
1064+
// Depending on how tests run, this can also just succeed without getting cancelled at all. Do nothing in this case.
1065+
Console.WriteLine("Task was not cancelled as expected.");
1066+
}
1067+
1068+
#endif
1069+
9721070
/// <summary>
9731071
/// Write a large amount of data and test cancellation
9741072
/// </summary>
@@ -1001,6 +1099,7 @@ public async Task Test_Async_ParseDataIntoDataItemsAlignment()
10011099
};
10021100
await plc.ReadMultipleVarsAsync(dataItems, CancellationToken.None);
10031101
}
1102+
10041103
#endregion
10051104
}
10061105
}

S7.Net.UnitTest/S7NetTestsSync.cs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
using S7.UnitTest.Helpers;
88
using System.Security.Cryptography;
99

10+
#if NET5_0_OR_GREATER
11+
using System.Buffers;
12+
#endif
13+
1014
#endregion
1115

1216
/**
@@ -778,6 +782,33 @@ public void T33_WriteLargeByteArray()
778782
CollectionAssert.AreEqual(data, readData);
779783
}
780784

785+
#if NET5_0_OR_GREATER
786+
787+
/// <summary>
788+
/// Write/Read a large amount of data to test PDU max
789+
/// </summary>
790+
[TestMethod]
791+
public void T33_WriteLargeByteArrayWithSpan()
792+
{
793+
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
794+
795+
var randomEngine = new Random();
796+
using var dataOwner = MemoryPool<byte>.Shared.Rent(8192);
797+
var data = dataOwner.Memory.Span.Slice(0, 8192);
798+
var db = 2;
799+
randomEngine.NextBytes(data);
800+
801+
plc.WriteBytes(DataType.DataBlock, db, 0, data);
802+
803+
using var readDataOwner = MemoryPool<byte>.Shared.Rent(data.Length);
804+
var readData = readDataOwner.Memory.Span.Slice(0, data.Length);
805+
plc.ReadBytes(readData, DataType.DataBlock, db, 0);
806+
807+
CollectionAssert.AreEqual(data.ToArray(), readData.ToArray());
808+
}
809+
810+
#endif
811+
781812
[TestMethod, ExpectedException(typeof(PlcException))]
782813
public void T18_ReadStructThrowsIfPlcIsNotConnected()
783814
{
@@ -1006,6 +1037,32 @@ public void T27_ReadWriteBytesMany()
10061037
}
10071038
}
10081039

1040+
#if NET5_0_OR_GREATER
1041+
1042+
[TestMethod]
1043+
public void T27_ReadWriteBytesManyWithSpan()
1044+
{
1045+
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
1046+
1047+
using var dataOwner = MemoryPool<byte>.Shared.Rent(2000);
1048+
var data = dataOwner.Memory.Span;
1049+
for (int i = 0; i < data.Length; i++)
1050+
data[i] = (byte)(i % 256);
1051+
1052+
plc.WriteBytes(DataType.DataBlock, 2, 0, data);
1053+
1054+
using var readDataOwner = MemoryPool<byte>.Shared.Rent(data.Length);
1055+
var readData = readDataOwner.Memory.Span.Slice(0, data.Length);
1056+
plc.ReadBytes(readData, DataType.DataBlock, 2, 0);
1057+
1058+
for (int x = 0; x < data.Length; x++)
1059+
{
1060+
Assert.AreEqual(x % 256, readData[x], $"Mismatch at offset {x}, expected {x % 256}, actual {readData[x]}.");
1061+
}
1062+
}
1063+
1064+
#endif
1065+
10091066
[TestMethod]
10101067
public void T28_ReadClass_DoesntCrash_When_ReadingLessThan1Byte()
10111068
{
@@ -1060,7 +1117,7 @@ public void T33_ReadWriteDateTimeLong()
10601117
Assert.AreEqual(test_value, test_value2, "Compare DateTimeLong Write/Read");
10611118
}
10621119

1063-
#endregion
1120+
#endregion
10641121

10651122
#region Private methods
10661123

S7.Net/PlcAsynchronous.cs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,34 @@ public async Task<byte[]> ReadBytesAsync(DataType dataType, int db, int startByt
122122
return resultBytes;
123123
}
124124

125+
#if NET5_0_OR_GREATER
126+
127+
/// <summary>
128+
/// Reads a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
129+
/// If the read was not successful, check LastErrorCode or LastErrorString.
130+
/// </summary>
131+
/// <param name="buffer">Buffer to receive the read bytes. The <see cref="Memory{T}.Length"/> determines the number of bytes to read.</param>
132+
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
133+
/// <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>
134+
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
135+
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
136+
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
137+
/// <returns>Returns the bytes in an array</returns>
138+
public async Task ReadBytesAsync(Memory<byte> buffer, DataType dataType, int db, int startByteAdr, CancellationToken cancellationToken = default)
139+
{
140+
int index = 0;
141+
while (buffer.Length > 0)
142+
{
143+
//This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0.
144+
var maxToRead = Math.Min(buffer.Length, MaxPDUSize - 18);
145+
await ReadBytesWithSingleRequestAsync(dataType, db, startByteAdr + index, buffer.Slice(0, maxToRead), cancellationToken).ConfigureAwait(false);
146+
buffer = buffer.Slice(maxToRead);
147+
index += maxToRead;
148+
}
149+
}
150+
151+
#endif
152+
125153
/// <summary>
126154
/// Read and decode a certain number of bytes of the "VarType" provided.
127155
/// This can be used to read multiple consecutive variables of the same type (Word, DWord, Int, etc).
@@ -320,6 +348,33 @@ public async Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, b
320348
}
321349
}
322350

351+
#if NET5_0_OR_GREATER
352+
353+
/// <summary>
354+
/// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
355+
/// If the write was not successful, check LastErrorCode or LastErrorString.
356+
/// </summary>
357+
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
358+
/// <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>
359+
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
360+
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
361+
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
362+
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
363+
/// <returns>A task that represents the asynchronous write operation.</returns>
364+
public async Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, ReadOnlyMemory<byte> value, CancellationToken cancellationToken = default)
365+
{
366+
int localIndex = 0;
367+
while (value.Length > 0)
368+
{
369+
var maxToWrite = (int)Math.Min(value.Length, MaxPDUSize - 35);
370+
await WriteBytesWithASingleRequestAsync(dataType, db, startByteAdr + localIndex, value.Slice(0, maxToWrite), cancellationToken).ConfigureAwait(false);
371+
value = value.Slice(maxToWrite);
372+
localIndex += maxToWrite;
373+
}
374+
}
375+
376+
#endif
377+
323378
/// <summary>
324379
/// Write a single bit from a DB with the specified index.
325380
/// </summary>
@@ -451,6 +506,20 @@ private async Task ReadBytesWithSingleRequestAsync(DataType dataType, int db, in
451506
Array.Copy(s7data, 18, buffer, offset, count);
452507
}
453508

509+
#if NET5_0_OR_GREATER
510+
511+
private async Task ReadBytesWithSingleRequestAsync(DataType dataType, int db, int startByteAdr, Memory<byte> buffer, CancellationToken cancellationToken)
512+
{
513+
var dataToSend = BuildReadRequestPackage(new[] { new DataItemAddress(dataType, db, startByteAdr, buffer.Length) });
514+
515+
var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
516+
AssertReadResponse(s7data, buffer.Length);
517+
518+
s7data.AsSpan(18, buffer.Length).CopyTo(buffer.Span);
519+
}
520+
521+
#endif
522+
454523
/// <summary>
455524
/// Write DataItem(s) to the PLC. Throws an exception if the response is invalid
456525
/// or when the PLC reports errors for item(s) written.
@@ -496,6 +565,37 @@ private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db,
496565
}
497566
}
498567

568+
#if NET5_0_OR_GREATER
569+
570+
/// <summary>
571+
/// Writes up to 200 bytes to the PLC. You must specify the memory area type, memory are address, byte start address and bytes count.
572+
/// </summary>
573+
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
574+
/// <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>
575+
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
576+
/// <param name="value">Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.</param>
577+
/// <returns>A task that represents the asynchronous write operation.</returns>
578+
private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, ReadOnlyMemory<byte> value, CancellationToken cancellationToken)
579+
{
580+
try
581+
{
582+
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value.Span);
583+
var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false);
584+
585+
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
586+
}
587+
catch (OperationCanceledException)
588+
{
589+
throw;
590+
}
591+
catch (Exception exc)
592+
{
593+
throw new PlcException(ErrorCode.WriteData, exc);
594+
}
595+
}
596+
597+
#endif
598+
499599
private async Task WriteBitWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue, CancellationToken cancellationToken)
500600
{
501601
try

0 commit comments

Comments
 (0)