From 50b026d7a5ca3c45dfa22241f409fec79e550786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20J=C3=A4ger?= Date: Thu, 19 Apr 2018 00:31:04 +0200 Subject: [PATCH] Read TPKT/COTP packets / Read MaxPDU size from PLC Read responses from the PLS using classes for TPKT and COPT. This makes the communication more robust. It will now handle empty COTP packets that SoftPLS and WinAC based PLCs send out. I use RFC names for functions and classes. Change logic to use COTP and S7Comm reponse codes instead of relying on packet sizes. Read Max PDU size from connection setup. Ref #21 Change logic to use MaxPDUSize when reading istead of hardcoded limit. I tried using MaxPDUSize when writing data but this failed when packet size is over 256 on snap7. So i decided to drop changes to write size. I have done some tests against WinAC cpu and it seems to handle bigger pdu's when writing if negotiated in the connection setup. This might just be a SNAP7 bug. Fix MaxPDUSize for readbytes Remove debug line Simplify byte copy. Remove unessesarry buffer --- S7.Net/COTP.cs | 97 ++++++++++++++++++++++++++++++++++++++ S7.Net/PLC.cs | 110 ++++++++++++++++++++++++------------------- S7.Net/S7.Net.csproj | 2 + S7.Net/TPKT.cs | 49 +++++++++++++++++++ 4 files changed, 209 insertions(+), 49 deletions(-) create mode 100644 S7.Net/COTP.cs create mode 100644 S7.Net/TPKT.cs diff --git a/S7.Net/COTP.cs b/S7.Net/COTP.cs new file mode 100644 index 0000000..434bd06 --- /dev/null +++ b/S7.Net/COTP.cs @@ -0,0 +1,97 @@ +using System; +using System.IO; +using System.Net.Sockets; + +namespace S7.Net +{ + + /// + /// COTP Protocol functions and types + /// + internal class COTP + { + /// + /// Describes a COTP TPDU (Transport protocol data unit) + /// + public class TPDU + { + public byte HeaderLength; + public byte PDUType; + public int TPDUNumber; + public byte[] Data; + public bool LastDataUnit; + + public TPDU(TPKT tPKT) + { + var br = new BinaryReader(new MemoryStream(tPKT.Data)); + HeaderLength = br.ReadByte(); + if (HeaderLength >= 2) + { + PDUType = br.ReadByte(); + if (PDUType == 0xf0) //DT Data + { + var flags = br.ReadByte(); + TPDUNumber = flags & 0x7F; + LastDataUnit = (flags & 0x80) > 0; + Data = br.ReadBytes(tPKT.Length - HeaderLength - 4); //4 = TPKT Size + return; + } + //TODO: Handle other PDUTypes + } + Data = new byte[0]; + } + + /// + /// Reads COTP TPDU (Transport protocol data unit) from the network stream + /// See: https://tools.ietf.org/html/rfc905 + /// + /// The socket to read from + /// COTP DPDU instance + public static TPDU Read(Socket socket) + { + var tpkt = TPKT.Read(socket); + if (tpkt.Length > 0) return new TPDU(tpkt); + return null; + } + + public override string ToString() + { + return string.Format("Length: {0} PDUType: {1} TPDUNumber: {2} Last: {3} Segment Data: {4}", + HeaderLength, + PDUType, + TPDUNumber, + LastDataUnit, + BitConverter.ToString(Data) + ); + } + + } + + /// + /// Describes a COTP TSDU (Transport service data unit). One TSDU consist of 1 ore more TPDUs + /// + public class TSDU + { + /// + /// Reads the full COTP TSDU (Transport service data unit) + /// See: https://tools.ietf.org/html/rfc905 + /// + /// Data in TSDU + public static byte[] Read(Socket socket) + { + var segment = TPDU.Read(socket); + if (segment == null) return null; + + var output = new MemoryStream(segment.Data.Length); + output.Write(segment.Data, 0, segment.Data.Length); + + while (!segment.LastDataUnit) + { + segment = TPDU.Read(socket); + output.Write(segment.Data, (int)output.Position, segment.Data.Length); + } + return output.GetBuffer(); + } + } + } +} diff --git a/S7.Net/PLC.cs b/S7.Net/PLC.cs index 662d9b3..d7a10aa 100644 --- a/S7.Net/PLC.cs +++ b/S7.Net/PLC.cs @@ -38,6 +38,11 @@ namespace S7.Net /// Slot of the CPU of the PLC /// public Int16 Slot { get; private set; } + + /// + /// Max PDU size this cpu supports + /// + public Int16 MaxPDUSize { get; set; } /// /// Returns true if a connection to the PLC can be established @@ -116,6 +121,7 @@ namespace S7.Net IP = ip; Rack = rack; Slot = slot; + MaxPDUSize = 240; } private ErrorCode Connect(Socket socket) @@ -157,7 +163,6 @@ namespace S7.Net /// Returns ErrorCode.NoError if the connection was successful, otherwise check the ErrorCode public ErrorCode Open() { - byte[] bReceive = new byte[256]; _mSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); _mSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 1000); @@ -168,9 +173,25 @@ namespace S7.Net return LastErrorCode; } - try + try { - byte[] bSend1 = { 3, 0, 0, 22, 17, 224, 0, 0, 0, 46, 0, 193, 2, 1, 0, 194, 2, 3, 0, 192, 1, 9 }; + byte[] bSend1 = { + 3, 0, 0, 22, //TPKT + 17, //COTP Header Length + 224, //Connect Request + 0, 0, //Destination Reference + 0, 46, //Source Reference + 0, //Flags + 193, //Parameter Code (src-tasp) + 2, //Parameter Length + 1, 0, //Source TASP + 194, //Parameter Code (dst-tasp) + 2, //Parameter Length + 3, 0, //Destination TASP + 192, //Parameter Code (tpdu-size) + 1, //Parameter Length + 9 //TPDU Size (2^9 = 512) + }; switch (CPU) { case CpuType.S7200: @@ -226,28 +247,29 @@ namespace S7.Net return ErrorCode.WrongCPU_Type; } + //COTP Setup _mSocket.Send(bSend1, 22, SocketFlags.None); - if (_mSocket.Receive(bReceive, 22, SocketFlags.None) != 22) - { - throw new Exception(ErrorCode.WrongNumberReceivedBytes.ToString()); - } + byte[] bsend2 = { 3, 0, 0, 25, 2, 240, 128, 50, 1, 0, 0, 255, 255, 0, 8, 0, 0, 240, 0, 0, 3, 0, 3, + 7, 80 //Try 1920 PDU Size. Same as libnodave. + }; - byte[] bsend2 = { 3, 0, 0, 25, 2, 240, 128, 50, 1, 0, 0, 255, 255, 0, 8, 0, 0, 240, 0, 0, 3, 0, 3, 1, 0 }; _mSocket.Send(bsend2, 25, SocketFlags.None); - if (_mSocket.Receive(bReceive, 27, SocketFlags.None) != 27) + var s7data = COTP.TSDU.Read(_mSocket); + if (s7data == null || s7data[1] != 0x03) //Check for S7 Ack Data { throw new Exception(ErrorCode.WrongNumberReceivedBytes.ToString()); - } + } + MaxPDUSize = (short)(s7data[18] * 256 + s7data[19]); + + return ErrorCode.NoError; } - catch(Exception exc) + catch (Exception exc) { LastErrorCode = ErrorCode.ConnectionError; LastErrorString = string.Format("Couldn't establish the connection to {0}.\nMessage: {1}", IP, exc.Message); return ErrorCode.ConnectionError; } - - return ErrorCode.NoError; } /// @@ -273,12 +295,14 @@ namespace S7.Net public void ReadMultipleVars(List dataItems) { int cntBytes = dataItems.Sum(dataItem => VarTypeToByteLength(dataItem.VarType, dataItem.Count)); - + + //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. if (dataItems.Count > 20) throw new Exception("Too many vars requested"); if (cntBytes > 222) throw new Exception("Too many bytes requested"); // TODO: proper TDU check + split in multiple requests - try { // first create the header @@ -293,25 +317,20 @@ namespace S7.Net _mSocket.Send(package.array, package.array.Length, SocketFlags.None); - byte[] bReceive = new byte[512]; - int numReceived = _mSocket.Receive(bReceive, 512, SocketFlags.None); - if (bReceive[21] != 0xff) + var s7data = COTP.TSDU.Read(_mSocket); + if (s7data == null || s7data[14] != 0xff) throw new Exception(ErrorCode.WrongNumberReceivedBytes.ToString()); - int offset = 25; + int offset = 18; foreach (var dataItem in dataItems) { int byteCnt = VarTypeToByteLength(dataItem.VarType, dataItem.Count); - byte[] bytes = new byte[byteCnt]; - - for (int i = 0; i < byteCnt; i++) - { - bytes[i] = bReceive[i + offset]; - } - + dataItem.Value = ParseBytes( + dataItem.VarType, + s7data.Skip(offset).Take(byteCnt).ToArray(), + dataItem.Count + ); offset += byteCnt + 4; - - dataItem.Value = ParseBytes(dataItem.VarType, bytes, dataItem.Count); } } catch (SocketException socketException) @@ -341,7 +360,8 @@ namespace S7.Net int index = startByteAdr; while (count > 0) { - var maxToRead = (int)Math.Min(count, 200); + //This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0. + var maxToRead = (int)Math.Min(count, MaxPDUSize-18); byte[] bytes = ReadBytesWithASingleRequest(dataType, db, index, maxToRead); if (bytes == null) return resultBytes.ToArray(); @@ -618,7 +638,10 @@ namespace S7.Net int localIndex = 0; int count = value.Length; while (count > 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 = (int)Math.Min(count, 200); ErrorCode lastError = WriteBytesWithASingleRequest(dataType, db, startByteAdr + localIndex, value.Skip(localIndex).Take(maxToWrite).ToArray()); if (lastError != ErrorCode.NoError) @@ -1032,8 +1055,6 @@ namespace S7.Net private byte[] ReadBytesWithASingleRequest(DataType dataType, int db, int startByteAdr, int count) { - byte[] bytes = new byte[count]; - try { // first create the header @@ -1045,15 +1066,11 @@ namespace S7.Net _mSocket.Send(package.array, package.array.Length, SocketFlags.None); - byte[] bReceive = new byte[512]; - int numReceived = _mSocket.Receive(bReceive, 512, SocketFlags.None); - if (bReceive[21] != 0xff) + var s7data = COTP.TSDU.Read(_mSocket); + if (s7data == null || s7data[14] != 0xff) throw new Exception(ErrorCode.WrongNumberReceivedBytes.ToString()); - for (int cnt = 0; cnt < count; cnt++) - bytes[cnt] = bReceive[cnt + 25]; - - return bytes; + return s7data.Skip(18).Take(count).ToArray(); } catch (SocketException socketException) { @@ -1080,9 +1097,7 @@ namespace S7.Net /// NoError if it was successful, or the error is specified private ErrorCode WriteBytesWithASingleRequest(DataType dataType, int db, int startByteAdr, byte[] value) { - byte[] bReceive = new byte[513]; int varCount = 0; - try { varCount = value.Length; @@ -1111,8 +1126,8 @@ namespace S7.Net _mSocket.Send(package.array, package.array.Length, SocketFlags.None); - int numReceived = _mSocket.Receive(bReceive, 512, SocketFlags.None); - if (bReceive[21] != 0xff) + var s7data = COTP.TSDU.Read(_mSocket); + if (s7data == null || s7data[14] != 0xff) { throw new Exception(ErrorCode.WrongNumberReceivedBytes.ToString()); } @@ -1129,7 +1144,6 @@ namespace S7.Net private ErrorCode WriteBitWithASingleRequest(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue) { - byte[] bReceive = new byte[513]; int varCount = 0; try @@ -1161,11 +1175,9 @@ namespace S7.Net _mSocket.Send(package.array, package.array.Length, SocketFlags.None); - int numReceived = _mSocket.Receive(bReceive, 512, SocketFlags.None); - if (bReceive[21] != 0xff) - { - throw new Exception(ErrorCode.WrongNumberReceivedBytes.ToString()); - } + var s7data = COTP.TSDU.Read(_mSocket); + if (s7data == null || s7data[14] != 0xff) + throw new Exception(ErrorCode.WrongNumberReceivedBytes.ToString()); return ErrorCode.NoError; } diff --git a/S7.Net/S7.Net.csproj b/S7.Net/S7.Net.csproj index ac2ac19..bb2abab 100644 --- a/S7.Net/S7.Net.csproj +++ b/S7.Net/S7.Net.csproj @@ -77,9 +77,11 @@ + + diff --git a/S7.Net/TPKT.cs b/S7.Net/TPKT.cs new file mode 100644 index 0000000..3ca9fd5 --- /dev/null +++ b/S7.Net/TPKT.cs @@ -0,0 +1,49 @@ +using System; +using System.Net.Sockets; + +namespace S7.Net +{ + + /// + /// Describes a TPKT Packet + /// + internal class TPKT + { + public byte Version; + public byte Reserved1; + public int Length; + public byte[] Data; + + /// + /// Reds a TPKT from the socket + /// + /// The socket to read from + /// TPKT Instace + public static TPKT Read(Socket socket) + { + var buf = new byte[4]; + socket.Receive(buf, 4, SocketFlags.None); + var pkt = new TPKT + { + Version = buf[0], + Reserved1 = buf[1], + Length = buf[2] * 256 + buf[3] //BigEndian + }; + if (pkt.Length > 0) + { + pkt.Data = new byte[pkt.Length - 4]; + socket.Receive(pkt.Data, pkt.Length - 4, SocketFlags.None); + } + return pkt; + } + + public override string ToString() + { + return string.Format("Version: {0} Length: {1} Data: {2}", + Version, + Length, + BitConverter.ToString(Data) + ); + } + } +}