diff --git a/S7.Net.UnitTest/S7NetTestsSync.cs b/S7.Net.UnitTest/S7NetTestsSync.cs index 5948171..6daa9f7 100644 --- a/S7.Net.UnitTest/S7NetTestsSync.cs +++ b/S7.Net.UnitTest/S7NetTestsSync.cs @@ -897,6 +897,21 @@ namespace S7.Net.UnitTest Assert.AreEqual(tc.Bool1, tc2.Bool1); } + [TestMethod] + public void T29_Read_Write_ExceptionHandlingWhenPlcIsNotReachable() + { + // leave plc Open + S7TestServer.Stop(); + + double test_value = 55.66; + plc.Write("DB1.DBD0", test_value); + Assert.AreEqual(plc.LastErrorCode, ErrorCode.WriteData, "No Write Error."); + + var helper = plc.Read("DB1.DBD0"); + Assert.AreEqual(helper, null, "Value in Read."); + Assert.AreEqual(plc.LastErrorCode, ErrorCode.ReadData, "No Read Error."); + } + #endregion #region Private methods diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs index 7525ff2..cb004e2 100644 --- a/S7.Net/PLCHelpers.cs +++ b/S7.Net/PLCHelpers.cs @@ -154,7 +154,7 @@ namespace S7.Net varType = VarType.Counter; return; default: - throw new InvalidAddressException(string.Format("{0} is not av valid address", address.Substring(0, 1))); + throw new InvalidAddressException(string.Format("{0} is not a valid address", address.Substring(0, 1))); } string txt2 = address.Substring(1); @@ -248,7 +248,7 @@ namespace S7.Net /// private object ParseBytes(VarType varType, byte[] bytes, int varCount, byte bitAdr = 0) { - if (bytes == null) + if (bytes == null || bytes.Length == 0) return null; switch (varType) @@ -316,46 +316,10 @@ 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 . - /// - public void ClearLastError() + /// + /// Sets the to and to . + /// + public void ClearLastError() { LastErrorCode = ErrorCode.NoError; LastErrorString = string.Empty; diff --git a/S7.Net/PlcAsynchronous.cs b/S7.Net/PlcAsynchronous.cs index efaab86..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 { @@ -234,12 +235,12 @@ namespace S7.Net } catch (SocketException socketException) { - LastErrorCode = ErrorCode.WriteData; + LastErrorCode = ErrorCode.ReadData; LastErrorString = socketException.Message; } catch (Exception exc) { - LastErrorCode = ErrorCode.WriteData; + LastErrorCode = ErrorCode.ReadData; LastErrorString = exc.Message; } return dataItems; @@ -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 b0ddcbd..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 @@ -272,7 +273,8 @@ namespace S7.Net ErrorCode lastError = WriteBitWithASingleRequest(dataType, db, startByteAdr, bitAdr, value); if (lastError != ErrorCode.NoError) { - return lastError; } + return lastError; + } return ErrorCode.NoError; } @@ -323,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)); } /// @@ -388,18 +390,33 @@ namespace S7.Net } catch (SocketException socketException) { - LastErrorCode = ErrorCode.WriteData; + LastErrorCode = ErrorCode.ReadData; LastErrorString = socketException.Message; return null; } catch (Exception exc) { - LastErrorCode = ErrorCode.WriteData; + LastErrorCode = ErrorCode.ReadData; LastErrorString = exc.Message; return null; } } + /// + /// 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; @@ -525,7 +542,7 @@ namespace S7.Net { package.Add(CreateReadDataRequestPackage(dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, VarTypeToByteLength(dataItem.VarType, dataItem.Count))); } - + stream.Write(package.Array, 0, package.Array.Length); var s7data = COTP.TSDU.Read(stream); //TODO use Async @@ -536,12 +553,12 @@ namespace S7.Net } catch (SocketException socketException) { - LastErrorCode = ErrorCode.WriteData; + LastErrorCode = ErrorCode.ReadData; LastErrorString = socketException.Message; } catch (Exception exc) { - LastErrorCode = ErrorCode.WriteData; + LastErrorCode = ErrorCode.ReadData; LastErrorString = exc.Message; } } 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 6b576f1..1bcf18e 100644 --- a/S7.Net/S7.Net.csproj +++ b/S7.Net/S7.Net.csproj @@ -1,4 +1,4 @@ - + @@ -7,5 +7,5 @@ Properties\S7.Net.snk S7.Net.UnitTest - + \ No newline at end of file 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); diff --git a/S7.Net/Types/Int.cs b/S7.Net/Types/Int.cs index 468e8b3..5489691 100644 --- a/S7.Net/Types/Int.cs +++ b/S7.Net/Types/Int.cs @@ -29,8 +29,8 @@ namespace S7.Net.Types { byte[] bytes = new byte[2]; - bytes[1] = (byte)((int)value & 0xFF); - bytes[0] = (byte)((int)value >> 8 & 0xFF); + bytes[0] = (byte) (value >> 8 & 0xFF); + bytes[1] = (byte)(value & 0xFF); return bytes; } @@ -45,8 +45,8 @@ namespace S7.Net.Types for(int i=0; i< value.Length; i++) { - bytes[bytesPos++] = (byte)((int)value[i] & 0xFF); - bytes[bytesPos++] = (byte)(((int)value[i] >> 8) & 0xFF); + bytes[bytesPos++] = (byte)((value[i] >> 8) & 0xFF); + bytes[bytesPos++] = (byte) (value[i] & 0xFF); } return bytes; }