diff --git a/S7.Net.UnitTest/S7NetTestsAsync.cs b/S7.Net.UnitTest/S7NetTestsAsync.cs
index 731e802f..5d598824 100644
--- a/S7.Net.UnitTest/S7NetTestsAsync.cs
+++ b/S7.Net.UnitTest/S7NetTestsAsync.cs
@@ -9,6 +9,11 @@
using System.Threading;
using System.Security.Cryptography;
+
+#if NET5_0_OR_GREATER
+using System.Buffers;
+#endif
+
#endregion
/**
@@ -139,6 +144,33 @@ public async Task Test_Async_WriteLargeByteArray()
CollectionAssert.AreEqual(data, readData);
}
+#if NET5_0_OR_GREATER
+
+ ///
+ /// Write/Read a large amount of data to test PDU max
+ ///
+ [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.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.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
+
///
/// Read/Write a class that has the same properties of a DB with the same field in the same order
///
@@ -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.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.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
+
///
/// Write a large amount of data and test cancellation
///
@@ -969,6 +1026,47 @@ public async Task Test_Async_WriteLargeByteArrayWithCancellation()
Console.WriteLine("Task was not cancelled as expected.");
}
+#if NET5_0_OR_GREATER
+
+ ///
+ /// Write a large amount of data and test cancellation
+ ///
+ [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.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
+
///
/// Write a large amount of data and test cancellation
///
@@ -1001,6 +1099,7 @@ public async Task Test_Async_ParseDataIntoDataItemsAlignment()
};
await plc.ReadMultipleVarsAsync(dataItems, CancellationToken.None);
}
+
#endregion
}
}
diff --git a/S7.Net.UnitTest/S7NetTestsSync.cs b/S7.Net.UnitTest/S7NetTestsSync.cs
index 76317c0f..85e22461 100644
--- a/S7.Net.UnitTest/S7NetTestsSync.cs
+++ b/S7.Net.UnitTest/S7NetTestsSync.cs
@@ -7,6 +7,10 @@
using S7.UnitTest.Helpers;
using System.Security.Cryptography;
+#if NET5_0_OR_GREATER
+using System.Buffers;
+#endif
+
#endregion
/**
@@ -778,6 +782,33 @@ public void T33_WriteLargeByteArray()
CollectionAssert.AreEqual(data, readData);
}
+#if NET5_0_OR_GREATER
+
+ ///
+ /// Write/Read a large amount of data to test PDU max
+ ///
+ [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.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.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()
{
@@ -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.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.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()
{
@@ -1060,7 +1117,7 @@ public void T33_ReadWriteDateTimeLong()
Assert.AreEqual(test_value, test_value2, "Compare DateTimeLong Write/Read");
}
- #endregion
+#endregion
#region Private methods
diff --git a/S7.Net/PlcAsynchronous.cs b/S7.Net/PlcAsynchronous.cs
index f75cf233..c4aeb90e 100644
--- a/S7.Net/PlcAsynchronous.cs
+++ b/S7.Net/PlcAsynchronous.cs
@@ -122,6 +122,34 @@ public async Task ReadBytesAsync(DataType dataType, int db, int startByt
return resultBytes;
}
+#if NET5_0_OR_GREATER
+
+ ///
+ /// 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.
+ ///
+ /// Buffer to receive the read bytes. The determines the number of bytes to read.
+ /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.
+ /// 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.
+ /// Start byte address. If you want to read DB1.DBW200, this is 200.
+ /// 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.
+ /// Returns the bytes in an array
+ public async Task ReadBytesAsync(Memory 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
+
///
/// 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).
@@ -320,6 +348,33 @@ public async Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, b
}
}
+#if NET5_0_OR_GREATER
+
+ ///
+ /// 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.
+ ///
+ /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.
+ /// 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.
+ /// Start byte address. If you want to write DB1.DBW200, this is 200.
+ /// Bytes to write. If more than 200, multiple requests will be made.
+ /// 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.
+ /// A task that represents the asynchronous write operation.
+ public async Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, ReadOnlyMemory 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
+
///
/// Write a single bit from a DB with the specified index.
///
@@ -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 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
+
///
/// Write DataItem(s) to the PLC. Throws an exception if the response is invalid
/// or when the PLC reports errors for item(s) written.
@@ -496,6 +565,37 @@ private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db,
}
}
+#if NET5_0_OR_GREATER
+
+ ///
+ /// Writes up to 200 bytes to the PLC. You must specify the memory area type, memory are address, byte start address and bytes count.
+ ///
+ /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.
+ /// 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.
+ /// Start byte address. If you want to read DB1.DBW200, this is 200.
+ /// Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.
+ /// A task that represents the asynchronous write operation.
+ private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, ReadOnlyMemory 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
diff --git a/S7.Net/PlcSynchronous.cs b/S7.Net/PlcSynchronous.cs
index 80e47381..0b035baa 100644
--- a/S7.Net/PlcSynchronous.cs
+++ b/S7.Net/PlcSynchronous.cs
@@ -51,6 +51,32 @@ public byte[] ReadBytes(DataType dataType, int db, int startByteAdr, int count)
return result;
}
+#if NET5_0_OR_GREATER
+
+ ///
+ /// 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.
+ ///
+ /// Buffer to receive the read bytes. The determines the number of bytes to read.
+ /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.
+ /// 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.
+ /// Start byte address. If you want to read DB1.DBW200, this is 200.
+ /// Returns the bytes in an array
+ public void ReadBytes(Span buffer, DataType dataType, int db, int startByteAdr)
+ {
+ 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);
+ ReadBytesWithSingleRequest(dataType, db, startByteAdr + index, buffer.Slice(0, maxToRead));
+ buffer = buffer.Slice(maxToRead);
+ index += maxToRead;
+ }
+ }
+
+#endif
+
///
/// 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).
@@ -193,6 +219,33 @@ public void WriteBytes(DataType dataType, int db, int startByteAdr, byte[] value
}
}
+#if NET5_0_OR_GREATER
+
+ ///
+ /// 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.
+ ///
+ /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.
+ /// 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.
+ /// Start byte address. If you want to write DB1.DBW200, this is 200.
+ /// Bytes to write. If more than 200, multiple requests will be made.
+ public void WriteBytes(DataType dataType, int db, int startByteAdr, ReadOnlySpan value)
+ {
+ int localIndex = 0;
+ while (value.Length > 0)
+ {
+ //TODO: Figure out how to use MaxPDUSize here
+ //Snap7 seems to choke on PDU sizes above 256 even if snap7
+ //replies with bigger PDU size in connection setup.
+ var maxToWrite = Math.Min(value.Length, MaxPDUSize - 28);//TODO tested only when the MaxPDUSize is 480
+ WriteBytesWithASingleRequest(dataType, db, startByteAdr + localIndex, value.Slice(0, maxToWrite));
+ value = value.Slice(maxToWrite);
+ localIndex += maxToWrite;
+ }
+ }
+
+#endif
+
///
/// Write a single bit from a DB with the specified index.
///
@@ -317,6 +370,33 @@ private void ReadBytesWithSingleRequest(DataType dataType, int db, int startByte
}
}
+#if NET5_0_OR_GREATER
+
+ private void ReadBytesWithSingleRequest(DataType dataType, int db, int startByteAdr, Span buffer)
+ {
+ try
+ {
+ // first create the header
+ int packageSize = 19 + 12; // 19 header + 12 for 1 request
+ var package = new System.IO.MemoryStream(packageSize);
+ BuildHeaderPackage(package);
+ // package.Add(0x02); // datenart
+ BuildReadDataRequestPackage(package, dataType, db, startByteAdr, buffer.Length);
+
+ var dataToSend = package.ToArray();
+ var s7data = RequestTsdu(dataToSend);
+ AssertReadResponse(s7data, buffer.Length);
+
+ s7data.AsSpan(18, buffer.Length).CopyTo(buffer);
+ }
+ catch (Exception exc)
+ {
+ throw new PlcException(ErrorCode.ReadData, exc);
+ }
+ }
+
+#endif
+
///
/// Write DataItem(s) to the PLC. Throws an exception if the response is invalid
/// or when the PLC reports errors for item(s) written.
@@ -349,6 +429,25 @@ private void WriteBytesWithASingleRequest(DataType dataType, int db, int startBy
}
}
+#if NET5_0_OR_GREATER
+
+ private void WriteBytesWithASingleRequest(DataType dataType, int db, int startByteAdr, ReadOnlySpan value)
+ {
+ try
+ {
+ var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value);
+ var s7data = RequestTsdu(dataToSend);
+
+ ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
+ }
+ catch (Exception exc)
+ {
+ throw new PlcException(ErrorCode.WriteData, exc);
+ }
+ }
+
+#endif
+
private byte[] BuildWriteBytesPackage(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count)
{
int varCount = count;
@@ -380,6 +479,41 @@ private byte[] BuildWriteBytesPackage(DataType dataType, int db, int startByteAd
return package.ToArray();
}
+#if NET5_0_OR_GREATER
+
+ private byte[] BuildWriteBytesPackage(DataType dataType, int db, int startByteAdr, ReadOnlySpan value)
+ {
+ int varCount = value.Length;
+ // first create the header
+ int packageSize = 35 + varCount;
+ var package = new MemoryStream(new byte[packageSize]);
+
+ package.WriteByte(3);
+ package.WriteByte(0);
+ //complete package size
+ package.WriteByteArray(Int.ToByteArray((short)packageSize));
+ package.WriteByteArray(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 });
+ package.WriteByteArray(Word.ToByteArray((ushort)(varCount - 1)));
+ package.WriteByteArray(new byte[] { 0, 0x0e });
+ package.WriteByteArray(Word.ToByteArray((ushort)(varCount + 4)));
+ package.WriteByteArray(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x02 });
+ package.WriteByteArray(Word.ToByteArray((ushort)varCount));
+ package.WriteByteArray(Word.ToByteArray((ushort)(db)));
+ package.WriteByte((byte)dataType);
+ var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
+ package.WriteByte((byte)overflow);
+ package.WriteByteArray(Word.ToByteArray((ushort)(startByteAdr * 8)));
+ package.WriteByteArray(new byte[] { 0, 4 });
+ package.WriteByteArray(Word.ToByteArray((ushort)(varCount * 8)));
+
+ // now join the header and the data
+ package.Write(value);
+
+ return package.ToArray();
+ }
+
+#endif
+
private byte[] BuildWriteBitPackage(DataType dataType, int db, int startByteAdr, bool bitValue, int bitAdr)
{
var value = new[] { bitValue ? (byte)1 : (byte)0 };