using S7.Net.Types; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Sockets; using System.Threading.Tasks; using S7.Net.Protocol; using System.Threading; using S7.Net.Protocol.S7; namespace S7.Net { /// /// Creates an instance of S7.Net driver /// public partial class Plc { /// /// Connects to the PLC and performs a COTP ConnectionRequest and S7 CommunicationSetup. /// /// The token to monitor for cancellation requests. The default value is None. /// Please note that the cancellation will not affect opening the socket in any way and only affects data transfers for configuring the connection after the socket connection is successfully established. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases. /// A task that represents the asynchronous open operation. public async Task OpenAsync(CancellationToken cancellationToken = default) { var stream = await ConnectAsync(cancellationToken).ConfigureAwait(false); try { await queue.Enqueue(async () => { cancellationToken.ThrowIfCancellationRequested(); await EstablishConnection(stream, cancellationToken).ConfigureAwait(false); _stream = stream; return default(object); }).ConfigureAwait(false); } catch (Exception) { stream.Dispose(); throw; } } private async Task ConnectAsync(CancellationToken cancellationToken) { tcpClient = new TcpClient(); ConfigureConnection(); #if NET5_0_OR_GREATER await tcpClient.ConnectAsync(IP, Port, cancellationToken).ConfigureAwait(false); #else await tcpClient.ConnectAsync(IP, Port).ConfigureAwait(false); #endif return tcpClient.GetStream(); } private async Task EstablishConnection(Stream stream, CancellationToken cancellationToken) { await RequestConnection(stream, cancellationToken).ConfigureAwait(false); await SetupConnection(stream, cancellationToken).ConfigureAwait(false); } private async Task RequestConnection(Stream stream, CancellationToken cancellationToken) { var requestData = ConnectionRequest.GetCOTPConnectionRequest(TsapPair); var response = await NoLockRequestTpduAsync(stream, requestData, cancellationToken).ConfigureAwait(false); if (response.PDUType != COTP.PduType.ConnectionConfirmed) { throw new InvalidDataException("Connection request was denied", response.TPkt.Data, 1, 0x0d); } } private async Task SetupConnection(Stream stream, CancellationToken cancellationToken) { var setupData = GetS7ConnectionSetup(); var s7data = await NoLockRequestTsduAsync(stream, setupData, 0, setupData.Length, cancellationToken) .ConfigureAwait(false); if (s7data.Length < 2) throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup"); //Check for S7 Ack Data if (s7data[1] != 0x03) throw new InvalidDataException("Error reading Communication Setup response", s7data, 1, 0x03); if (s7data.Length < 20) throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup"); // TODO: check if this should not rather be UInt16. MaxPDUSize = s7data[18] * 256 + s7data[19]; } /// /// 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. /// /// 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. /// Byte count, if you want to read 120 bytes, set this to 120. /// 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(DataType dataType, int db, int startByteAdr, int count, CancellationToken cancellationToken = default) { var resultBytes = new byte[count]; await ReadBytesAsync(resultBytes, dataType, db, startByteAdr, cancellationToken).ConfigureAwait(false); return resultBytes; } /// /// 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; } } /// /// 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). /// If the read 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 read DB1.DBW200, this is 200. /// Type of the variable/s that you are reading /// Address of bit. If you want to read DB1.DBX200.6, set 6 to this parameter. /// /// 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. public async Task ReadAsync(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0, CancellationToken cancellationToken = default) { int cntBytes = VarTypeToByteLength(varType, varCount); byte[] bytes = await ReadBytesAsync(dataType, db, startByteAdr, cntBytes, cancellationToken).ConfigureAwait(false); return ParseBytes(varType, bytes, varCount, bitAdr); } /// /// Reads a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. /// If the read was not successful, check LastErrorCode or LastErrorString. /// /// Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. /// 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 an object that contains the value. This object must be cast accordingly. public async Task ReadAsync(string variable, CancellationToken cancellationToken = default) { var adr = new PLCAddress(variable); return await ReadAsync(adr.DataType, adr.DbNumber, adr.StartByte, adr.VarType, 1, (byte)adr.BitNumber, cancellationToken).ConfigureAwait(false); } /// /// Reads all the bytes needed to fill a struct in C#, starting from a certain address, and return an object that can be casted to the struct. /// /// Type of the struct to be readed (es.: TypeOf(MyStruct)). /// Address of the DB. /// 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 a struct that must be cast. public async Task ReadStructAsync(Type structType, int db, int startByteAdr = 0, CancellationToken cancellationToken = default) { int numBytes = Types.Struct.GetStructSize(structType); // now read the package var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes, cancellationToken).ConfigureAwait(false); // and decode it return Types.Struct.FromBytes(structType, resultBytes); } /// /// Reads all the bytes needed to fill a struct in C#, starting from a certain address, and returns the struct or null if nothing was read. /// /// The struct type /// Address of the DB. /// 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 a nulable struct. If nothing was read null will be returned. public async Task ReadStructAsync(int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : struct { return await ReadStructAsync(typeof(T), db, startByteAdr, cancellationToken).ConfigureAwait(false) as T?; } /// /// 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. /// /// Instance of the class that will store the values /// Index of the DB; es.: 1 is for DB1 /// 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. /// The number of read bytes public async Task> ReadClassAsync(object sourceClass, int db, int startByteAdr = 0, CancellationToken cancellationToken = default) { int numBytes = (int)Class.GetClassSize(sourceClass); if (numBytes <= 0) { throw new Exception("The size of the class is less than 1 byte and therefore cannot be read"); } // now read the package var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes, cancellationToken).ConfigureAwait(false); // and decode it Class.FromBytes(sourceClass, resultBytes); return new Tuple(resultBytes.Length, sourceClass); } /// /// 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 /// type, the class needs a default constructor. /// /// The class that will be instantiated. Requires a default constructor /// Index of the DB; es.: 1 is for DB1 /// 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. /// An instance of the class with the values read from the PLC. If no data has been read, null will be returned public async Task ReadClassAsync(int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : class { return await ReadClassAsync(() => Activator.CreateInstance(), db, startByteAdr, cancellationToken).ConfigureAwait(false); } /// /// 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. /// /// The class that will be instantiated /// Function to instantiate the class /// Index of the DB; es.: 1 is for DB1 /// 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. /// An instance of the class with the values read from the PLC. If no data has been read, null will be returned public async Task ReadClassAsync(Func classFactory, int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : class { var instance = classFactory(); var res = await ReadClassAsync(instance, db, startByteAdr, cancellationToken).ConfigureAwait(false); int readBytes = res.Item1; if (readBytes <= 0) { return null; } return (T)res.Item2; } /// /// 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. /// 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. /// The number of DataItems as well as the total size of the requested data can not exceed a certain limit (protocol restriction). /// /// List of dataitems that contains the list of variables that must be read. /// 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. public async Task> ReadMultipleVarsAsync(List dataItems, CancellationToken cancellationToken = default) { //Snap7 seems to choke on PDU sizes above 256 even if snap7 //replies with bigger PDU size in connection setup. AssertPduSizeForRead(dataItems); try { var dataToSend = BuildReadRequestPackage(dataItems.Select(d => DataItem.GetDataItemAddress(d)).ToList()); var s7data = await RequestTsduAsync(dataToSend, cancellationToken); ValidateResponseCode((ReadWriteErrorCode)s7data[14]); ParseDataIntoDataItems(s7data, dataItems); } catch (SocketException socketException) { throw new PlcException(ErrorCode.ReadData, socketException); } catch (OperationCanceledException) { throw; } catch (Exception exc) { throw new PlcException(ErrorCode.ReadData, exc); } return dataItems; } /// /// Read the PLC clock value. /// /// 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 operation, with it's result set to the current PLC time on completion. public async Task ReadClockAsync(CancellationToken cancellationToken = default) { var request = BuildClockReadRequest(); var response = await RequestTsduAsync(request, cancellationToken); return ParseClockReadResponse(response); } /// /// Write the PLC clock value. /// /// The date and time to set the PLC clock to /// 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 operation. public async Task WriteClockAsync(System.DateTime value, CancellationToken cancellationToken = default) { var request = BuildClockWriteRequest(value); var response = await RequestTsduAsync(request, cancellationToken); ParseClockWriteResponse(response); } /// /// Read the current status from the PLC. A value of 0x08 indicates the PLC is in run status, regardless of the PLC type. /// /// 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 operation, with it's result set to the current PLC status on completion. public async Task ReadStatusAsync(CancellationToken cancellationToken = default) { var dataToSend = BuildSzlReadRequestPackage(0x0424, 0); var s7data = await RequestTsduAsync(dataToSend, cancellationToken); return (byte) (s7data[37] & 0x0f); } /// /// 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 Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, byte[] value, CancellationToken cancellationToken = default) { return WriteBytesAsync(dataType, db, startByteAdr, value.AsMemory(), cancellationToken); } /// /// 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; } } /// /// Write a single bit from a DB with the specified index. /// /// 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. /// The address of the bit. (0-7) /// 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 WriteBitAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool value, CancellationToken cancellationToken = default) { 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)); await WriteBitWithASingleRequestAsync(dataType, db, startByteAdr, bitAdr, value, cancellationToken).ConfigureAwait(false); } /// /// Write a single bit from a DB with the specified index. /// /// 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. /// The address of the bit. (0-7) /// 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 WriteBitAsync(DataType dataType, int db, int startByteAdr, int bitAdr, int value, CancellationToken cancellationToken = default) { if (value < 0 || value > 1) throw new ArgumentException("Value must be 0 or 1", nameof(value)); await WriteBitAsync(dataType, db, startByteAdr, bitAdr, value == 1, cancellationToken).ConfigureAwait(false); } /// /// Takes in input an object and tries to parse it to an array of values. This can be used to write many data, all of the same type. /// You must specify the memory area type, memory are address, byte start address and bytes count. /// If the read 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 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. /// The address of the bit. (0-7) /// 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 WriteAsync(DataType dataType, int db, int startByteAdr, object value, int bitAdr = -1, CancellationToken cancellationToken = default) { if (bitAdr != -1) { //Must be writing a bit value as bitAdr is specified if (value is bool boolean) { await WriteBitAsync(dataType, db, startByteAdr, bitAdr, boolean, cancellationToken).ConfigureAwait(false); } else if (value is int intValue) { if (intValue < 0 || intValue > 7) throw new ArgumentOutOfRangeException( string.Format( "Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid", bitAdr), nameof(bitAdr)); 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 await WriteBytesAsync(dataType, db, startByteAdr, Serialization.SerializeValue(value), cancellationToken).ConfigureAwait(false); } /// /// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. /// /// Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. /// Value to be written to the PLC /// 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 WriteAsync(string variable, object value, CancellationToken cancellationToken = default) { var adr = new PLCAddress(variable); await WriteAsync(adr.DataType, adr.DbNumber, adr.StartByte, value, adr.BitNumber, cancellationToken).ConfigureAwait(false); } /// /// Writes a C# struct to a DB in the PLC /// /// The struct to be written /// Db address /// Start bytes on the PLC /// 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 WriteStructAsync(object structValue, int db, int startByteAdr = 0, CancellationToken cancellationToken = default) { var bytes = Struct.ToBytes(structValue).ToList(); await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes.ToArray(), cancellationToken).ConfigureAwait(false); } /// /// Writes a C# class to a DB in the PLC /// /// The class to be written /// Db address /// Start bytes on the PLC /// 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 WriteClassAsync(object classValue, int db, int startByteAdr = 0, CancellationToken cancellationToken = default) { byte[] bytes = new byte[(int)Class.GetClassSize(classValue)]; Types.Class.ToBytes(classValue, bytes); await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes, cancellationToken).ConfigureAwait(false); } 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); } /// /// Write DataItem(s) to the PLC. Throws an exception if the response is invalid /// or when the PLC reports errors for item(s) written. /// /// The DataItem(s) to write to the PLC. /// Task that completes when response from PLC is parsed. public async Task WriteAsync(params DataItem[] dataItems) { AssertPduSizeForWrite(dataItems); var message = new ByteArray(); var length = S7WriteMultiple.CreateRequest(message, dataItems); var response = await RequestTsduAsync(message.Array, 0, length).ConfigureAwait(false); S7WriteMultiple.ParseResponse(response, response.Length, dataItems); } /// /// 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 cancellation token that can be used to cancel the asynchronous operation. /// 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); } } private async Task WriteBitWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue, CancellationToken cancellationToken) { try { var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr); var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false); ValidateResponseCode((ReadWriteErrorCode)s7data[14]); } catch (OperationCanceledException) { throw; } catch (Exception exc) { throw new PlcException(ErrorCode.WriteData, exc); } } private Task RequestTsduAsync(byte[] requestData, CancellationToken cancellationToken = default) => RequestTsduAsync(requestData, 0, requestData.Length, cancellationToken); private Task RequestTsduAsync(byte[] requestData, int offset, int length, CancellationToken cancellationToken = default) { var stream = GetStreamIfAvailable(); return queue.Enqueue(() => NoLockRequestTsduAsync(stream, requestData, offset, length, cancellationToken)); } private async Task NoLockRequestTpduAsync(Stream stream, byte[] requestData, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); try { using var closeOnCancellation = cancellationToken.Register(Close); await stream.WriteAsync(requestData, 0, requestData.Length, cancellationToken).ConfigureAwait(false); return await COTP.TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false); } catch (Exception exc) { if (exc is TPDUInvalidException || exc is TPKTInvalidException) { Close(); } throw; } } private async Task NoLockRequestTsduAsync(Stream stream, byte[] requestData, int offset, int length, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); try { using var closeOnCancellation = cancellationToken.Register(Close); await stream.WriteAsync(requestData, offset, length, cancellationToken).ConfigureAwait(false); return await COTP.TSDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false); } catch (Exception exc) { if (exc is TPDUInvalidException || exc is TPKTInvalidException) { Close(); } throw; } } } }