diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs index ab59d2c..cb004e2 100644 --- a/S7.Net/PLCHelpers.cs +++ b/S7.Net/PLCHelpers.cs @@ -316,42 +316,6 @@ namespace S7.Net } } - public byte[] GetPackage(object value) - { - switch (value.GetType().Name) - { - case "Byte": - return Types.Byte.ToByteArray((byte)value); - case "Int16": - return Types.Int.ToByteArray((Int16)value); - case "UInt16": - return Types.Word.ToByteArray((UInt16)value); - case "Int32": - return Types.DInt.ToByteArray((Int32)value); - case "UInt32": - return Types.DWord.ToByteArray((UInt32)value); - case "Double": - return Types.Double.ToByteArray((double)value); - case "Byte[]": - return (byte[])value; - case "Int16[]": - return Types.Int.ToByteArray((Int16[])value); - case "UInt16[]": - return Types.Word.ToByteArray((UInt16[])value); - case "Int32[]": - return Types.DInt.ToByteArray((Int32[])value); - case "UInt32[]": - return Types.DWord.ToByteArray((UInt32[])value); - case "Double[]": - return Types.Double.ToByteArray((double[])value); - case "String": - return Types.String.ToByteArray(value as string); - default: - throw new InvalidVariableTypeException(); - } - } - - /// /// Sets the to and to . /// diff --git a/S7.Net/PlcAsynchronous.cs b/S7.Net/PlcAsynchronous.cs index ec5d18a..d6c639e 100644 --- a/S7.Net/PlcAsynchronous.cs +++ b/S7.Net/PlcAsynchronous.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using System.Linq; using System.Net.Sockets; using System.Threading.Tasks; +using S7.Net.Protocol; namespace S7.Net { @@ -344,7 +345,7 @@ namespace S7.Net } throw new ArgumentException("Value must be a bool or an int to write a bit", nameof(value)); } - return await WriteBytesAsync(dataType, db, startByteAdr, GetPackage(value)); + return await WriteBytesAsync(dataType, db, startByteAdr, Serialization.SerializeValue(value)); } /// @@ -411,6 +412,22 @@ namespace S7.Net return bytes; } + /// + /// 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) + { + var message = new ByteArray(); + var length = S7WriteMultiple.CreateRequest(message, dataItems); + await stream.WriteAsync(message.Array, 0, length).ConfigureAwait(false); + + var response = await COTP.TSDU.ReadAsync(stream).ConfigureAwait(false); + S7WriteMultiple.ParseResponse(response, response.Length, dataItems); + } + /// /// Writes up to 200 bytes to the PLC and returns NoError if successful. You must specify the memory area type, memory are address, byte start address and bytes count. /// If the write was not successful, check LastErrorCode or LastErrorString. diff --git a/S7.Net/PlcSynchronous.cs b/S7.Net/PlcSynchronous.cs index 892d58b..ccd4279 100644 --- a/S7.Net/PlcSynchronous.cs +++ b/S7.Net/PlcSynchronous.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using System.Linq; using System.Net.Sockets; using System.Threading.Tasks; +using S7.Net.Protocol; //Implement obsolete synchronous methods here namespace S7.Net @@ -324,7 +325,7 @@ namespace S7.Net } throw new ArgumentException("Value must be a bool or an int to write a bit", nameof(value)); } - return WriteBytes(dataType, db, startByteAdr, GetPackage(value)); + return WriteBytes(dataType, db, startByteAdr, Serialization.SerializeValue(value)); } /// @@ -401,6 +402,21 @@ namespace S7.Net } } + /// + /// 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. + public void Write(params DataItem[] dataItems) + { + var message = new ByteArray(); + var length = S7WriteMultiple.CreateRequest(message, dataItems); + stream.Write(message.Array, 0, length); + + var response = COTP.TSDU.Read(stream); + S7WriteMultiple.ParseResponse(response, response.Length, dataItems); + } + private ErrorCode WriteBytesWithASingleRequest(DataType dataType, int db, int startByteAdr, byte[] value) { int varCount = 0; diff --git a/S7.Net/Protocol/S7WriteMultiple.cs b/S7.Net/Protocol/S7WriteMultiple.cs new file mode 100644 index 0000000..f4b2457 --- /dev/null +++ b/S7.Net/Protocol/S7WriteMultiple.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using S7.Net.Types; + +namespace S7.Net.Protocol +{ + internal static class S7WriteMultiple + { + public static int CreateRequest(ByteArray message, DataItem[] dataItems) + { + message.Add(Header.Template); + + message[Header.Offsets.ParameterCount] = (byte) dataItems.Length; + var paramSize = dataItems.Length * Parameter.Template.Length; + + Serialization.SetWordAt(message, Header.Offsets.ParameterSize, + (ushort) (2 + paramSize)); + + var paramOffset = Header.Template.Length; + var dataOffset = paramOffset + paramSize; + var data = new ByteArray(); + + foreach (var item in dataItems) + { + message.Add(Parameter.Template); + var value = Serialization.SerializeValue(item.Value); + var wordLen = item.Value is bool ? 1 : 2; + + message[paramOffset + Parameter.Offsets.WordLength] = (byte) wordLen; + Serialization.SetWordAt(message, paramOffset + Parameter.Offsets.Amount, (ushort) value.Length); + Serialization.SetWordAt(message, paramOffset + Parameter.Offsets.DbNumber, (ushort) item.DB); + message[paramOffset + Parameter.Offsets.Area] = (byte) item.DataType; + Serialization.SetAddressAt(message, paramOffset + Parameter.Offsets.Address, item.StartByteAdr, item.BitAdr); + + paramOffset += Parameter.Template.Length; + + data.Add(0x00); + if (item.Value is bool b) + { + data.Add(0x03); + data.AddWord(1); + + data.Add(b ? (byte) 1 : (byte) 0); + data.Add(0); + } + else + { + var len = value.Length; + data.Add(0x04); + data.AddWord((ushort) (len << 3)); + data.Add(value); + + if ((len & 0b1) == 1) + { + data.Add(0); + } + } + } + + + message.Add(data.Array); + + Serialization.SetWordAt(message, Header.Offsets.MessageLength, (ushort) message.Length); + Serialization.SetWordAt(message, Header.Offsets.DataLength, (ushort) (message.Length - paramOffset)); + + return message.Length; + } + + public static void ParseResponse(byte[] message, int length, DataItem[] dataItems) + { + if (length < 12) throw new Exception("Not enough data received to parse write response."); + + var messageError = Serialization.GetWordAt(message, 10); + if (messageError != 0) + throw new Exception($"Write failed with error {messageError}."); + + if (length < 14 + dataItems.Length) + throw new Exception("Not enough data received to parse individual item responses."); + + IList itemResults = new ArraySegment(message, 14, dataItems.Length); + + List errors = null; + + for (int i = 0; i < dataItems.Length; i++) + { + var result = itemResults[i]; + if (result == 0xff) continue; + + if (errors == null) errors = new List(); + errors.Add(new Exception($"Write of dataItem {dataItems[i]} failed with error code {result}.")); + } + + if (errors != null) + throw new AggregateException( + $"Write failed for {errors.Count} items. See the innerExceptions for details.", errors); + } + + private static class Header + { + public static byte[] Template { get; } = + { + 0x03, 0x00, 0x00, 0x00, // TPKT + 0x02, 0xf0, 0x80, // ISO DT + 0x32, // S7 protocol ID + 0x01, // JobRequest + 0x00, 0x00, // Reserved + 0x05, 0x00, // PDU reference + 0x00, 0x0e, // Parameters length + 0x00, 0x00, // Data length + 0x05, // Function: Write var + 0x00, // Number of items to write + }; + + public static class Offsets + { + public const int MessageLength = 2; + public const int ParameterSize = 13; + public const int DataLength = 15; + public const int ParameterCount = 18; + } + } + + private static class Parameter + { + public static byte[] Template { get; } = + { + 0x12, // Spec + 0x0a, // Length of remaining bytes + 0x10, // Addressing mode + 0x02, // Transport size + 0x00, 0x00, // Number of elements + 0x00, 0x00, // DB number + 0x84, // Area type + 0x00, 0x00, 0x00 // Area offset + }; + + public static class Offsets + { + public const int WordLength = 3; + public const int Amount = 4; + public const int DbNumber = 6; + public const int Area = 8; + public const int Address = 9; + } + } + } +} diff --git a/S7.Net/Protocol/Serialization.cs b/S7.Net/Protocol/Serialization.cs new file mode 100644 index 0000000..f586731 --- /dev/null +++ b/S7.Net/Protocol/Serialization.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using S7.Net.Types; + +namespace S7.Net.Protocol +{ + internal static class Serialization + { + public static ushort GetWordAt(IList buf, int index) + { + return (ushort) ((buf[index] << 8) + buf[index]); + } + + public static byte[] SerializeValue(object value) + { + switch (value.GetType().Name) + { + case "Boolean": + return new[] {(byte) ((bool) value ? 1 : 0)}; + case "Byte": + return Types.Byte.ToByteArray((byte) value); + case "Int16": + return Types.Int.ToByteArray((Int16) value); + case "UInt16": + return Types.Word.ToByteArray((UInt16) value); + case "Int32": + return Types.DInt.ToByteArray((Int32) value); + case "UInt32": + return Types.DWord.ToByteArray((UInt32) value); + case "Double": + return Types.Double.ToByteArray((double) value); + case "Byte[]": + return (byte[]) value; + case "Int16[]": + return Types.Int.ToByteArray((Int16[]) value); + case "UInt16[]": + return Types.Word.ToByteArray((UInt16[]) value); + case "Int32[]": + return Types.DInt.ToByteArray((Int32[]) value); + case "UInt32[]": + return Types.DWord.ToByteArray((UInt32[]) value); + case "Double[]": + return Types.Double.ToByteArray((double[]) value); + case "String": + return Types.String.ToByteArray(value as string); + default: + throw new InvalidVariableTypeException(); + } + } + + public static void SetAddressAt(ByteArray buffer, int index, int startByte, byte bitNumber) + { + var start = startByte * 8 + bitNumber; + buffer[index + 2] = (byte) start; + start = start >> 8; + buffer[index + 1] = (byte) start; + start = start >> 8; + buffer[index] = (byte) start; + } + + public static void SetWordAt(ByteArray buffer, int index, ushort value) + { + buffer[index] = (byte) (value >> 8); + buffer[index + 1] = (byte) value; + } + } +} diff --git a/S7.Net/S7.Net.csproj b/S7.Net/S7.Net.csproj index 7b9f625..08bf12b 100644 --- a/S7.Net/S7.Net.csproj +++ b/S7.Net/S7.Net.csproj @@ -85,6 +85,8 @@ + + diff --git a/S7.Net/Types/ByteArray.cs b/S7.Net/Types/ByteArray.cs index ca8456a..322cee8 100644 --- a/S7.Net/Types/ByteArray.cs +++ b/S7.Net/Types/ByteArray.cs @@ -6,11 +6,19 @@ namespace S7.Net.Types { List list = new List(); + public byte this[int index] + { + get => list[index]; + set => list[index] = value; + } + public byte[] Array { get { return list.ToArray(); } } + public int Length => list.Count; + public ByteArray() { list = new List(); @@ -31,6 +39,12 @@ namespace S7.Net.Types list.Add(item); } + public void AddWord(ushort value) + { + list.Add((byte) (value >> 8)); + list.Add((byte) value); + } + public void Add(byte[] items) { list.AddRange(items);