diff --git a/src/DriverTest/Program.cs b/src/DriverTest/Program.cs index aea7ba1..1cfe01a 100644 --- a/src/DriverTest/Program.cs +++ b/src/DriverTest/Program.cs @@ -38,9 +38,9 @@ static void Main(string[] args) Console.WriteLine("Main - Versuche Verbindungsaufbau zu: " + HostIp); S7CommPlusConnection conn = new S7CommPlusConnection(); - res = conn.Connect(HostIp, Password, Username); - if (res == 0) + try { + conn.Connect(HostIp, Password, Username); Console.WriteLine("Main - Connect fertig"); #region Variablenhaushalt browsen @@ -95,7 +95,7 @@ static void Main(string[] args) List values = new List(); List errors = new List(); - + // Fehlerhafte Variable setzen //readlist[2].LID[0] = 123; res = conn.ReadValues(readlist, out values, out errors); @@ -112,7 +112,7 @@ static void Main(string[] args) string formatstring = "{0,-80}{1,-30}{2,-20}{3,-20}"; Console.WriteLine(String.Format(formatstring, "SYMBOLIC-NAME", "ACCESS-SEQUENCE", "TYP", "VALUE")); for (int i = 0; i < vars.Count; i++) - { + { string s = String.Format(formatstring, vars[i].Name, vars[i].AccessSequence, Softdatatype.Types[vars[i].Softdatatype], values[i]); Console.WriteLine(s); } @@ -167,12 +167,12 @@ static void Main(string[] args) conn.SetPlcOperatingState(3); #endregion */ - conn.Disconnect(); } - else + catch (Exception e) { - Console.WriteLine("Main - Connect fehlgeschlagen!"); + Console.WriteLine("Main - " + e.ToString()); } + conn.Disconnect(); Console.WriteLine("Main - ENDE. Bitte Taste drücken."); Console.ReadKey(); } diff --git a/src/S7CommPlusDriver/Alarming/BrowseAlarms.cs b/src/S7CommPlusDriver/Alarming/BrowseAlarms.cs index 6f31c4f..a7f96b5 100644 --- a/src/S7CommPlusDriver/Alarming/BrowseAlarms.cs +++ b/src/S7CommPlusDriver/Alarming/BrowseAlarms.cs @@ -332,6 +332,74 @@ private void GetTexts(byte[] tloa_1, byte[] tloa_2, byte[] tloa_3, byte[] tlsa, } } } + + /// + /// Reads the active program alarms from the Plc (single poll). + /// + /// Call example: + /// CultureInfo ci = new CultureInfo("de-DE"); + /// var alarmList = new List(); + /// conn.GetActiveAlarms(out alarmList, ci.LCID); + /// foreach (var a in alarmList) + /// { + /// Console.WriteLine(a.ToString()); + /// } + /// + /// Contains the alarms, empty if there is no active alarm + /// Language id for retrieving the text entries, use language code e.g. 1031 for german + /// 0 on success + public int GetActiveAlarms(out List alarmList, int languageId) + { + int res; + + alarmList = new List(); + + var exploreReq = new ExploreRequest(ProtocolVersion.V2); + exploreReq.ExploreId = Ids.NativeObjects_theAlarmSubsystem_Rid; + exploreReq.ExploreRequestId = Ids.AlarmSubsystem_itsUpdateRelevantDAI; + exploreReq.ExploreChildsRecursive = 1; + exploreReq.ExploreParents = 0; + + // Add the requestes attributes. + // Request the same attributes we get from an alarm notification, so we can reuse other methods. + exploreReq.AddressList.Add(Ids.DAI_CPUAlarmID); + exploreReq.AddressList.Add(Ids.DAI_AllStatesInfo); + exploreReq.AddressList.Add(Ids.DAI_AlarmDomain); + exploreReq.AddressList.Add(Ids.DAI_Coming); + exploreReq.AddressList.Add(Ids.DAI_Going); + exploreReq.AddressList.Add(Ids.DAI_MessageType); + exploreReq.AddressList.Add(Ids.DAI_HmiInfo); + // Extra ones which we only need for compatibility with notification. + exploreReq.AddressList.Add(Ids.ObjectVariableTypeName); + exploreReq.AddressList.Add(Ids.DAI_SequenceCounter); + exploreReq.AddressList.Add(Ids.DAI_AlarmTexts_Rid); + + res = SendS7plusFunctionObject(exploreReq); + if (res != 0) + { + return res; + } + m_LastError = 0; + WaitForNewS7plusReceived(m_ReadTimeout); + if (m_LastError != 0) + { + return m_LastError; + } + + var exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); + res = checkResponseWithIntegrity(exploreReq, exploreRes); + if (res != 0) + { + return res; + } + + foreach (var obj in exploreRes.Objects) + { + alarmList.Add(AlarmsDai.FromNotificationObject(obj, languageId)); + } + + return 0; + } } public class AlarmData diff --git a/src/S7CommPlusDriver/ClientApi/PlcTag.cs b/src/S7CommPlusDriver/ClientApi/PlcTag.cs index 18cc628..4e83546 100644 --- a/src/S7CommPlusDriver/ClientApi/PlcTag.cs +++ b/src/S7CommPlusDriver/ClientApi/PlcTag.cs @@ -1,5 +1,7 @@ using System; using System.Text; +using System.Collections; +using System.Collections.Generic; namespace S7CommPlusDriver.ClientApi { @@ -1701,4 +1703,711 @@ public override string ToString() return String.Format(fmt, Value.ToString(), ns); } } + + #region Arrays + + public class PlcTagBoolArray : PlcTag + { + private bool[] m_Value; + + public bool[] Value + { + get { return m_Value; } + set { m_Value = value; } + } + + public PlcTagBoolArray(string name, ItemAddress address, uint softdatatype) : base(name, address, softdatatype) + { + m_Value = new bool[1]; + } + + public override void ProcessReadResult(object valueObj, ulong error) + { + LastReadError = error; + if (CheckErrorAndType(error, valueObj, typeof(ValueBoolArray)) == 0) + { + Value = ((ValueBoolArray)valueObj).GetValue(); + + Quality = PlcTagQC.TAG_QUALITY_GOOD; + } + else + { + Quality = PlcTagQC.TAG_QUALITY_BAD; + } + } + + public override PValue GetWriteValue() + { + return new ValueBoolArray(Value); + } + + public override string ToString() + { + var val = new ValueBoolArray(Value); + return ResultString(this, val.ToString()); + } + } + + public class PlcTagByteArray : PlcTag + { + private byte[] m_Value; + + public byte[] Value + { + get { return m_Value; } + set { m_Value = value; } + } + + public PlcTagByteArray(string name, ItemAddress address, uint softdatatype) : base(name, address, softdatatype) + { + m_Value = new byte[1]; + } + + public override void ProcessReadResult(object valueObj, ulong error) + { + LastReadError = error; + if (CheckErrorAndType(error, valueObj, typeof(ValueByteArray)) == 0) + { + Value = ((ValueByteArray)valueObj).GetValue(); + + Quality = PlcTagQC.TAG_QUALITY_GOOD; + } + else + { + Quality = PlcTagQC.TAG_QUALITY_BAD; + } + } + + public override PValue GetWriteValue() + { + return new ValueByteArray(Value); + } + + public override string ToString() + { + var val = new ValueByteArray(Value); + return ResultString(this, val.ToString()); + } + } + + public class PlcTagWordArray : PlcTag + { + private ushort[] m_Value; + + public ushort[] Value + { + get { return m_Value; } + set { m_Value = value; } + } + + public PlcTagWordArray(string name, ItemAddress address, uint softdatatype) : base(name, address, softdatatype) + { + m_Value = new ushort[1]; + } + + public override void ProcessReadResult(object valueObj, ulong error) + { + LastReadError = error; + if (CheckErrorAndType(error, valueObj, typeof(ValueWordArray)) == 0) + { + Value = ((ValueWordArray)valueObj).GetValue(); + + Quality = PlcTagQC.TAG_QUALITY_GOOD; + } + else + { + Quality = PlcTagQC.TAG_QUALITY_BAD; + } + } + + public override PValue GetWriteValue() + { + return new ValueWordArray(Value); + } + + public override string ToString() + { + var val = new ValueWordArray(Value); + return ResultString(this, val.ToString()); + } + } + + public class PlcTagIntArray : PlcTag + { + private short[] m_Value; + + public short[] Value + { + get { return m_Value; } + set { m_Value = value; } + } + + public PlcTagIntArray(string name, ItemAddress address, uint softdatatype) : base(name, address, softdatatype) + { + m_Value = new short[1]; + } + + public override void ProcessReadResult(object valueObj, ulong error) + { + LastReadError = error; + if (CheckErrorAndType(error, valueObj, typeof(ValueIntArray)) == 0) + { + Value = ((ValueIntArray)valueObj).GetValue(); + + Quality = PlcTagQC.TAG_QUALITY_GOOD; + } + else + { + Quality = PlcTagQC.TAG_QUALITY_BAD; + } + } + + public override PValue GetWriteValue() + { + return new ValueIntArray(Value); + } + + public override string ToString() + { + var val = new ValueIntArray(Value); + return ResultString(this, val.ToString()); + } + } + + public class PlcTagDWordArray : PlcTag + { + private uint[] m_Value; + + public uint[] Value + { + get { return m_Value; } + set { m_Value = value; } + } + + public PlcTagDWordArray(string name, ItemAddress address, uint softdatatype) : base(name, address, softdatatype) + { + m_Value = new uint[1]; + } + + public override void ProcessReadResult(object valueObj, ulong error) + { + LastReadError = error; + if (CheckErrorAndType(error, valueObj, typeof(ValueDWordArray)) == 0) + { + Value = ((ValueDWordArray)valueObj).GetValue(); + + Quality = PlcTagQC.TAG_QUALITY_GOOD; + } + else + { + Quality = PlcTagQC.TAG_QUALITY_BAD; + } + } + + public override PValue GetWriteValue() + { + return new ValueDWordArray(Value); + } + + public override string ToString() + { + var val = new ValueDWordArray(Value); + return ResultString(this, val.ToString()); + } + } + + public class PlcTagDIntArray : PlcTag + { + private int[] m_Value; + + public int[] Value + { + get { return m_Value; } + set { m_Value = value; } + } + + public PlcTagDIntArray(string name, ItemAddress address, uint softdatatype) : base(name, address, softdatatype) + { + m_Value = new int[1]; + } + + public override void ProcessReadResult(object valueObj, ulong error) + { + LastReadError = error; + if (CheckErrorAndType(error, valueObj, typeof(ValueDIntArray)) == 0) + { + Value = ((ValueDIntArray)valueObj).GetValue(); + + Quality = PlcTagQC.TAG_QUALITY_GOOD; + } + else + { + Quality = PlcTagQC.TAG_QUALITY_BAD; + } + } + + public override PValue GetWriteValue() + { + return new ValueDIntArray(Value); + } + + public override string ToString() + { + var val = new ValueDIntArray(Value); + return ResultString(this, val.ToString()); + } + } + + public class PlcTagRealArray : PlcTag + { + private float[] m_Value; + + public float[] Value + { + get { return m_Value; } + set { m_Value = value; } + } + + public PlcTagRealArray(string name, ItemAddress address, uint softdatatype) : base(name, address, softdatatype) + { + m_Value = new float[1]; + } + + public override void ProcessReadResult(object valueObj, ulong error) + { + LastReadError = error; + if (CheckErrorAndType(error, valueObj, typeof(ValueRealArray)) == 0) + { + Value = ((ValueRealArray)valueObj).GetValue(); + + Quality = PlcTagQC.TAG_QUALITY_GOOD; + } + else + { + Quality = PlcTagQC.TAG_QUALITY_BAD; + } + } + + public override PValue GetWriteValue() + { + return new ValueRealArray(Value); + } + + public override string ToString() + { + var val = new ValueRealArray(Value); + return ResultString(this, val.ToString()); + } + } + + public class PlcTagUSIntArray : PlcTag + { + private byte[] m_Value; + + public byte[] Value + { + get { return m_Value; } + set { m_Value = value; } + } + + public PlcTagUSIntArray(string name, ItemAddress address, uint softdatatype) : base(name, address, softdatatype) + { + m_Value = new byte[1]; + } + + public override void ProcessReadResult(object valueObj, ulong error) + { + LastReadError = error; + if (CheckErrorAndType(error, valueObj, typeof(ValueUSIntArray)) == 0) + { + Value = ((ValueUSIntArray)valueObj).GetValue(); + + Quality = PlcTagQC.TAG_QUALITY_GOOD; + } + else + { + Quality = PlcTagQC.TAG_QUALITY_BAD; + } + } + + public override PValue GetWriteValue() + { + return new ValueUSIntArray(Value); + } + + public override string ToString() + { + var val = new ValueUSIntArray(Value); + return ResultString(this, val.ToString()); + } + } + + public class PlcTagUIntArray : PlcTag + { + private ushort[] m_Value; + + public ushort[] Value + { + get { return m_Value; } + set { m_Value = value; } + } + + public PlcTagUIntArray(string name, ItemAddress address, uint softdatatype) : base(name, address, softdatatype) + { + m_Value = new ushort[1]; + } + + public override void ProcessReadResult(object valueObj, ulong error) + { + LastReadError = error; + if (CheckErrorAndType(error, valueObj, typeof(ValueUIntArray)) == 0) + { + Value = ((ValueUIntArray)valueObj).GetValue(); + + Quality = PlcTagQC.TAG_QUALITY_GOOD; + } + else + { + Quality = PlcTagQC.TAG_QUALITY_BAD; + } + } + + public override PValue GetWriteValue() + { + return new ValueUIntArray(Value); + } + + public override string ToString() + { + var val = new ValueUIntArray(Value); + return ResultString(this, val.ToString()); + } + } + + public class PlcTagUDIntArray : PlcTag + { + private uint[] m_Value; + + public uint[] Value + { + get { return m_Value; } + set { m_Value = value; } + } + + public PlcTagUDIntArray(string name, ItemAddress address, uint softdatatype) : base(name, address, softdatatype) + { + m_Value = new uint[1]; + } + + public override void ProcessReadResult(object valueObj, ulong error) + { + LastReadError = error; + if (CheckErrorAndType(error, valueObj, typeof(ValueUDIntArray)) == 0) + { + Value = ((ValueUDIntArray)valueObj).GetValue(); + + Quality = PlcTagQC.TAG_QUALITY_GOOD; + } + else + { + Quality = PlcTagQC.TAG_QUALITY_BAD; + } + } + + public override PValue GetWriteValue() + { + return new ValueUDIntArray(Value); + } + + public override string ToString() + { + var val = new ValueUDIntArray(Value); + return ResultString(this, val.ToString()); + } + } + + public class PlcTagSIntArray : PlcTag + { + private sbyte[] m_Value; + + public sbyte[] Value + { + get { return m_Value; } + set { m_Value = value; } + } + + public PlcTagSIntArray(string name, ItemAddress address, uint softdatatype) : base(name, address, softdatatype) + { + m_Value = new sbyte[1]; + } + + public override void ProcessReadResult(object valueObj, ulong error) + { + LastReadError = error; + if (CheckErrorAndType(error, valueObj, typeof(ValueSIntArray)) == 0) + { + Value = ((ValueSIntArray)valueObj).GetValue(); + + Quality = PlcTagQC.TAG_QUALITY_GOOD; + } + else + { + Quality = PlcTagQC.TAG_QUALITY_BAD; + } + } + + public override PValue GetWriteValue() + { + return new ValueSIntArray(Value); + } + + public override string ToString() + { + var val = new ValueSIntArray(Value); + return ResultString(this, val.ToString()); + } + } + + public class PlcTagDateAndTimeArray : PlcTag + { + /* BCD coded: + * YYMMDDhhmmssuuuQ + * uuu = milliseconds + * Q = Weekday 1=Su, 2=Mo, 3=Tu, 4=We, 5=Th, 6=Fr, 7=Sa + */ + private DateTime[] m_Value; + + public DateTime[] Value + { + get + { + return m_Value; + } + + set + { + bool dataOk = true; + foreach (var item in value) + { + if (item < new DateTime(1990, 1, 1) && item >= new DateTime(2090, 1, 1)) + { + dataOk = false; + break; + } + } + if (dataOk) + { + m_Value = value; + } + else + { + throw new ArgumentOutOfRangeException("Value", "DateTime must be >= 1990-01-01 and < 2090-01-01"); + } + } + } + + public PlcTagDateAndTimeArray(string name, ItemAddress address, uint softdatatype) : base(name, address, softdatatype) + { + Value = new DateTime[0]; + } + + public override void ProcessReadResult(object valueObj, ulong error) + { + LastReadError = error; + if (CheckErrorAndType(error, valueObj, typeof(ValueUSIntArray)) == 0) + { + List dateTimes = new List(); + var v = ((ValueUSIntArray)valueObj).GetValue(); + int pos = 0; + do + { + int[] ts = new int[8]; + for (int i = 0; i < 7; i++) + { + ts[i] = BcdByteToInt(v[pos + i]); + } + // The left nibble of the last byte contains the LSD of milliseconds, + // the right nibble the weekday (which we don't process here). + ts[7] = v[7] >> 4; + + int year; + if (ts[0] >= 90) + { + year = 1900 + ts[0]; + } + else + { + year = 2000 + ts[0]; + } + var value = new DateTime(year, ts[1], ts[2], ts[3], ts[4], ts[5]); + value = value.AddMilliseconds(ts[6] * 10 + ts[7]); + dateTimes.Add(value); + pos += 8; + } while (pos < v.Length); + Value = dateTimes.ToArray(); + Quality = PlcTagQC.TAG_QUALITY_GOOD; + } + else + { + Quality = PlcTagQC.TAG_QUALITY_BAD; + } + } + + public override PValue GetWriteValue() + { + var byteStrings = new List(); + foreach (var item in Value) + { + int[] ts = new int[8]; + byte[] b = new byte[8]; + if (item.Year < 2000) + { + // 90-99 = 1990-1999 + ts[0] = item.Year - 1900; + } + else + { + // 00-89 = 2000-2089 + ts[0] = item.Year - 2000; + } + ts[1] = item.Month; + ts[2] = item.Day; + ts[3] = item.Hour; + ts[4] = item.Minute; + ts[5] = item.Second; + ts[6] = item.Millisecond / 10; + ts[7] = (item.Millisecond % 10) << 4; // Don't set the weekday + for (int i = 0; i < 7; i++) + { + b[i] = IntToBcdByte(ts[i]); + } + b[7] = (byte)ts[7]; + byteStrings.AddRange(b); + } + return new ValueUSIntArray(byteStrings.ToArray()); + } + + public override string ToString() + { + string s = ""; + for (int i = 0; i < Value.Length; i++) + { + string ts = Value[i].ToString(); + if (Value[i].Millisecond > 0) + { + ts += String.Format(".{0:D03}", Value[i].Millisecond); + } + s += String.Format("{0}", ts); + } + s += ""; + return ResultString(this, s); + } + } + + + public class PlcTagStringArray : PlcTag + { + private string[] m_Value; + private byte m_MaxLength = 254; + private string m_Encoding = "ISO-8859-1"; + + public string[] Value + { + get + { + return m_Value; + } + + set + { + bool lengthOk = true; + foreach (var item in value) + { + if (item.Length > m_MaxLength) + { + lengthOk = false; + break; + } + } + if (lengthOk) + { + m_Value = value; + } + else + { + throw new ArgumentOutOfRangeException("Value", "String is longer than the allowed max. length of " + m_MaxLength); + } + } + } + + public PlcTagStringArray(string name, ItemAddress address, uint softdatatype, byte maxlength = 254) : base(name, address, softdatatype) + { + m_MaxLength = maxlength; + } + + public override void ProcessReadResult(object valueObj, ulong error) + { + LastReadError = error; + if (CheckErrorAndType(error, valueObj, typeof(ValueUSIntArray)) == 0) + { + List strings = new List(); + var v = ((ValueUSIntArray)valueObj).GetValue(); + int pos = 0; + do + { + int max_len = v[pos]; + int act_len = v[pos + 1]; + // IEC 61131-3 states ISO-646 IRV, with optional extensions like "Latin-1 Supplement". + // Siemens TIA-Portal gives warnings using other than 7 Bit ASCII characters. + // Let the user define his local encoding via SetStringEncoding(). + var str = Encoding.GetEncoding(m_Encoding).GetString(v, pos + 2, act_len); + strings.Add(str); + pos += max_len + 2; + + } while (pos < v.Length); + Value = strings.ToArray(); + Quality = PlcTagQC.TAG_QUALITY_GOOD; + } + else + { + Quality = PlcTagQC.TAG_QUALITY_BAD; + } + } + + public override PValue GetWriteValue() + { + var byteStrings = new List(); + foreach (var item in Value) + { + // Must write the complete array of MaxLength of the string (plus two bytes header). + byte[] sb = Encoding.GetEncoding(m_Encoding).GetBytes(item); + var b = new byte[m_MaxLength + 2]; + b[0] = m_MaxLength; + b[1] = (byte)sb.Length; + for (int i = 0; i < sb.Length; i++) + { + b[i + 2] = sb[i]; + } + byteStrings.AddRange(b); + } + return new ValueUSIntArray(byteStrings.ToArray()); + } + + public void SetStringEncoding(string encoding) + { + m_Encoding = encoding; + } + + public override string ToString() + { + string s = ""; + for (int i = 0; i < Value.Length; i++) + { + s += String.Format("{0}", Value[i]); + } + s += ""; + return ResultString(this, s); + } + } + #endregion } \ No newline at end of file diff --git a/src/S7CommPlusDriver/ClientApi/PlcTags.cs b/src/S7CommPlusDriver/ClientApi/PlcTags.cs index cfaa5ea..787cae6 100644 --- a/src/S7CommPlusDriver/ClientApi/PlcTags.cs +++ b/src/S7CommPlusDriver/ClientApi/PlcTags.cs @@ -66,25 +66,39 @@ public static int WriteTags(this S7CommPlusConnection conn, IEnumerable return res; } - public static PlcTag TagFactory(string name, ItemAddress address, uint softdatatype) + public static PlcTag TagFactory(string name, ItemAddress address, uint softdatatype, bool Is1Dim = false) { switch (softdatatype) { case Softdatatype.S7COMMP_SOFTDATATYPE_BOOL: + if (Is1Dim) + return new PlcTagBoolArray(name, address, softdatatype); return new PlcTagBool(name, address, softdatatype); case Softdatatype.S7COMMP_SOFTDATATYPE_BYTE: + if (Is1Dim) + return new PlcTagByteArray(name, address, softdatatype); return new PlcTagByte(name, address, softdatatype); case Softdatatype.S7COMMP_SOFTDATATYPE_CHAR: return new PlcTagChar(name, address, softdatatype); case Softdatatype.S7COMMP_SOFTDATATYPE_WORD: + if (Is1Dim) + return new PlcTagWordArray(name, address, softdatatype); return new PlcTagWord(name, address, softdatatype); case Softdatatype.S7COMMP_SOFTDATATYPE_INT: + if (Is1Dim) + return new PlcTagIntArray(name, address, softdatatype); return new PlcTagInt(name, address, softdatatype); case Softdatatype.S7COMMP_SOFTDATATYPE_DWORD: + if (Is1Dim) + return new PlcTagDWordArray(name, address, softdatatype); return new PlcTagDWord(name, address, softdatatype); case Softdatatype.S7COMMP_SOFTDATATYPE_DINT: + if (Is1Dim) + return new PlcTagDIntArray(name, address, softdatatype); return new PlcTagDInt(name, address, softdatatype); case Softdatatype.S7COMMP_SOFTDATATYPE_REAL: + if (Is1Dim) + return new PlcTagRealArray(name, address, softdatatype); return new PlcTagReal(name, address, softdatatype); case Softdatatype.S7COMMP_SOFTDATATYPE_DATE: return new PlcTagDate(name, address, softdatatype); @@ -95,9 +109,13 @@ public static PlcTag TagFactory(string name, ItemAddress address, uint softdatat case Softdatatype.S7COMMP_SOFTDATATYPE_S5TIME: return new PlcTagS5Time(name, address, softdatatype); case Softdatatype.S7COMMP_SOFTDATATYPE_DATEANDTIME: + if (Is1Dim) + return new PlcTagDateAndTimeArray(name, address, softdatatype); return new PlcTagDateAndTime(name, address, softdatatype); case Softdatatype.S7COMMP_SOFTDATATYPE_STRING: + if (Is1Dim) + return new PlcTagStringArray(name, address, softdatatype); return new PlcTagString(name, address, softdatatype); case Softdatatype.S7COMMP_SOFTDATATYPE_POINTER: return new PlcTagPointer(name, address, softdatatype); @@ -115,6 +133,8 @@ public static PlcTag TagFactory(string name, ItemAddress address, uint softdatat return new PlcTagUInt(name, address, softdatatype); case Softdatatype.S7COMMP_SOFTDATATYPE_BBOOL: + if (Is1Dim) + return new PlcTagBoolArray(name, address, softdatatype); return new PlcTagBool(name, address, softdatatype); case Softdatatype.S7COMMP_SOFTDATATYPE_LREAL: @@ -126,12 +146,20 @@ public static PlcTag TagFactory(string name, ItemAddress address, uint softdatat case Softdatatype.S7COMMP_SOFTDATATYPE_LWORD: return new PlcTagLWord(name, address, softdatatype); case Softdatatype.S7COMMP_SOFTDATATYPE_USINT: + if (Is1Dim) + return new PlcTagUSIntArray(name, address, softdatatype); return new PlcTagUSInt(name, address, softdatatype); case Softdatatype.S7COMMP_SOFTDATATYPE_UINT: + if (Is1Dim) + return new PlcTagUIntArray(name, address, softdatatype); return new PlcTagUInt(name, address, softdatatype); case Softdatatype.S7COMMP_SOFTDATATYPE_UDINT: + if (Is1Dim) + return new PlcTagUDIntArray(name, address, softdatatype); return new PlcTagUDInt(name, address, softdatatype); case Softdatatype.S7COMMP_SOFTDATATYPE_SINT: + if (Is1Dim) + return new PlcTagSIntArray(name, address, softdatatype); return new PlcTagSInt(name, address, softdatatype); case Softdatatype.S7COMMP_SOFTDATATYPE_WCHAR: diff --git a/src/S7CommPlusDriver/Core/Ids.cs b/src/S7CommPlusDriver/Core/Ids.cs index 610ca0d..0bca5fb 100644 --- a/src/S7CommPlusDriver/Core/Ids.cs +++ b/src/S7CommPlusDriver/Core/Ids.cs @@ -1,85 +1,86 @@ -#region License -/****************************************************************************** - * S7CommPlusDriver - * - * Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de - * - * This file is part of S7CommPlusDriver. - * - * S7CommPlusDriver is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - /****************************************************************************/ -#endregion - -namespace S7CommPlusDriver -{ - public static class Ids - { +#region License +/****************************************************************************** + * S7CommPlusDriver + * + * Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de + * + * This file is part of S7CommPlusDriver. + * + * S7CommPlusDriver is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + /****************************************************************************/ +#endregion + +namespace S7CommPlusDriver +{ + public static class Ids + { public const int None = 0; - public const int NativeObjects_theASRoot_Rid = 1; + public const int NativeObjects_theASRoot_Rid = 1; public const int NativeObjects_thePLCProgram_Rid = 3; + public const int NativeObjects_theAlarmSubsystem_Rid = 8; public const int NativeObjects_theCPUProxy_Rid = 49; - public const int NativeObjects_theCPUCommon_Rid = 50; - public const int NativeObjects_theCPUexecUnit_Rid = 52; - public const int NativeObjects_theIArea_Rid = 80; - public const int NativeObjects_theQArea_Rid = 81; - public const int NativeObjects_theMArea_Rid = 82; - public const int NativeObjects_theS7Counters_Rid = 83; - public const int NativeObjects_theS7Timers_Rid = 84; - public const int ObjectRoot = 201; - public const int GetNewRIDOnServer = 211; - public const int ObjectVariableTypeParentObject = 229; - public const int ObjectVariableTypeName = 233; - public const int ClassSubscriptions = 255; - public const int ClassServerSessionContainer = 284; - public const int ObjectServerSessionContainer = 285; - public const int ClassServerSession = 287; - public const int ObjectNullServerSession = 288; - public const int ServerSessionClientRID = 300; - public const int ServerSessionRequest = 303; - public const int ServerSessionResponse = 304; - public const int ServerSessionVersion = 306; - public const int LID_SessionVersionSystemPAOMString = 319; - public const int ClassTypeInfo = 511; - public const int ClassOMSTypeInfoContainer = 534; - public const int ObjectOMSTypeInfoContainer = 537; - public const int TextLibraryClassRID = 606; - public const int TextLibraryOffsetArea = 608; - public const int TextLibraryStringArea = 609; - public const int ClassSubscription = 1001; - public const int SubscriptionMissedSendings = 1002; - public const int SubscriptionSubsystemError = 1003; - public const int SubscriptionReferenceTriggerAndTransmitMode = 1005; - public const int SystemLimits = 1037; - public const int SubscriptionRouteMode = 1040; - public const int SubscriptionActive = 1041; - public const int Legitimate = 1846; - public const int SubscriptionReferenceList = 1048; - public const int SubscriptionCycleTime = 1049; - public const int SubscriptionDelayTime = 1050; - public const int SubscriptionDisabled = 1051; - public const int SubscriptionCount = 1052; - public const int SubscriptionCreditLimit = 1053; - public const int SubscriptionTicks = 1054; - public const int FreeItems = 1081; - public const int SubscriptionFunctionClassId = 1082; - public const int Filter = 1246; - public const int FilterOperation = 1247; - public const int AddressCount = 1249; - public const int Address = 1250; - public const int FilterValue = 1251; - public const int ObjectQualifier = 1256; - public const int ParentRID = 1257; - public const int CompositionAID = 1258; - public const int KeyQualifier = 1259; - public const int TI_TComSize = 1502; - public const int EffectiveProtectionLevel = 1842; - public const int ActiveProtectionLevel = 1843; - public const int CPUexecUnit_operatingStateReq = 2167; + public const int NativeObjects_theCPUCommon_Rid = 50; + public const int NativeObjects_theCPUexecUnit_Rid = 52; + public const int NativeObjects_theIArea_Rid = 80; + public const int NativeObjects_theQArea_Rid = 81; + public const int NativeObjects_theMArea_Rid = 82; + public const int NativeObjects_theS7Counters_Rid = 83; + public const int NativeObjects_theS7Timers_Rid = 84; + public const int ObjectRoot = 201; + public const int GetNewRIDOnServer = 211; + public const int ObjectVariableTypeParentObject = 229; + public const int ObjectVariableTypeName = 233; + public const int ClassSubscriptions = 255; + public const int ClassServerSessionContainer = 284; + public const int ObjectServerSessionContainer = 285; + public const int ClassServerSession = 287; + public const int ObjectNullServerSession = 288; + public const int ServerSessionClientRID = 300; + public const int ServerSessionRequest = 303; + public const int ServerSessionResponse = 304; + public const int ServerSessionVersion = 306; + public const int LID_SessionVersionSystemPAOMString = 319; + public const int ClassTypeInfo = 511; + public const int ClassOMSTypeInfoContainer = 534; + public const int ObjectOMSTypeInfoContainer = 537; + public const int TextLibraryClassRID = 606; + public const int TextLibraryOffsetArea = 608; + public const int TextLibraryStringArea = 609; + public const int ClassSubscription = 1001; + public const int SubscriptionMissedSendings = 1002; + public const int SubscriptionSubsystemError = 1003; + public const int SubscriptionReferenceTriggerAndTransmitMode = 1005; + public const int SystemLimits = 1037; + public const int SubscriptionRouteMode = 1040; + public const int SubscriptionActive = 1041; + public const int Legitimate = 1846; + public const int SubscriptionReferenceList = 1048; + public const int SubscriptionCycleTime = 1049; + public const int SubscriptionDelayTime = 1050; + public const int SubscriptionDisabled = 1051; + public const int SubscriptionCount = 1052; + public const int SubscriptionCreditLimit = 1053; + public const int SubscriptionTicks = 1054; + public const int FreeItems = 1081; + public const int SubscriptionFunctionClassId = 1082; + public const int Filter = 1246; + public const int FilterOperation = 1247; + public const int AddressCount = 1249; + public const int Address = 1250; + public const int FilterValue = 1251; + public const int ObjectQualifier = 1256; + public const int ParentRID = 1257; + public const int CompositionAID = 1258; + public const int KeyQualifier = 1259; + public const int TI_TComSize = 1502; + public const int EffectiveProtectionLevel = 1842; + public const int ActiveProtectionLevel = 1843; + public const int CPUexecUnit_operatingStateReq = 2167; public const int ASRoot_ItsFolders = 2468; - public const int PLCProgram_Class_Rid = 2520; + public const int PLCProgram_Class_Rid = 2520; public const int Block_BlockNumber = 2521; public const int Block_AutoNumbering = 2522; public const int Block_BlockLanguage = 2523; @@ -96,13 +97,13 @@ public static class Ids public const int Block_FunctionalSignature = 7589; public const int Block_AdditionalMAC = 7831; public const int Block_FailsafeBlockInfo = 7843; - public const int Block_FailsafeIFRHash = 7955; - public const int ASObjectES_IdentES = 2449; + public const int Block_FailsafeIFRHash = 7955; + public const int ASObjectES_IdentES = 2449; public const int Block_BodyDescription = 2533; - public const int DataInterface_InterfaceDescription = 2544; - public const int DataInterface_LineComments = 2546; - public const int DB_ValueInitial = 2548; - public const int DB_ValueActual = 2550; + public const int DataInterface_InterfaceDescription = 2544; + public const int DataInterface_LineComments = 2546; + public const int DB_ValueInitial = 2548; + public const int DB_ValueActual = 2550; public const int DB_InitialChanged = 2551; public const int DB_Class_Rid = 2574; public const int FB_Class_Rid = 2578; @@ -113,72 +114,73 @@ public static class Ids public const int FunctionalObject_intRefData = 2583; public const int FunctionalObject_NetworkComments = 2584; public const int FunctionalObject_NetworkTitles = 2585; - public const int FunctionalObject_CalleeList = 2586; - public const int FunctionalObject_InterfaceSignature = 2587; - public const int FunctionalObject_DisplayInfo = 2588; - public const int FunctionalObject_DebugInfo = 2589; - public const int FunctionalObject_LocalErrorhandling = 2590; - public const int FunctionalObject_LongConstants = 2591; + public const int FunctionalObject_CalleeList = 2586; + public const int FunctionalObject_InterfaceSignature = 2587; + public const int FunctionalObject_DisplayInfo = 2588; + public const int FunctionalObject_DebugInfo = 2589; + public const int FunctionalObject_LocalErrorhandling = 2590; + public const int FunctionalObject_LongConstants = 2591; public const int FunctionalObject_Class_Rid = 2592; - public const int OB_StartInfoType = 2607; - public const int OB_Class_Rid = 2610; - public const int AlarmSubscriptionRef_AlarmDomain = 2659; - public const int AlarmSubscriptionRef_itsAlarmSubsystem = 2660; - public const int AlarmSubscriptionRef_Class_Rid = 2662; - public const int DAI_CPUAlarmID = 2670; - public const int DAI_AllStatesInfo = 2671; - public const int DAI_AlarmDomain = 2672; - public const int DAI_Coming = 2673; - public const int DAI_Going = 2677; - public const int DAI_Class_Rid = 2681; - public const int DAI_AlarmTexts_Rid = 2715; - public const int AS_CGS_AllStatesInfo = 3474; - public const int AS_CGS_Timestamp = 3475; - public const int AS_CGS_AssociatedValues = 3476; - public const int AS_CGS_AckTimestamp = 3646; - public const int ControllerArea_ValueInitial = 3735; - public const int ControllerArea_ValueActual = 3736; + public const int OB_StartInfoType = 2607; + public const int OB_Class_Rid = 2610; + public const int AlarmSubscriptionRef_AlarmDomain = 2659; + public const int AlarmSubscriptionRef_itsAlarmSubsystem = 2660; + public const int AlarmSubscriptionRef_Class_Rid = 2662; + public const int AlarmSubsystem_itsUpdateRelevantDAI = 2667; + public const int DAI_CPUAlarmID = 2670; + public const int DAI_AllStatesInfo = 2671; + public const int DAI_AlarmDomain = 2672; + public const int DAI_Coming = 2673; + public const int DAI_Going = 2677; + public const int DAI_Class_Rid = 2681; + public const int DAI_AlarmTexts_Rid = 2715; + public const int AS_CGS_AllStatesInfo = 3474; + public const int AS_CGS_Timestamp = 3475; + public const int AS_CGS_AssociatedValues = 3476; + public const int AS_CGS_AckTimestamp = 3646; + public const int ControllerArea_ValueInitial = 3735; + public const int ControllerArea_ValueActual = 3736; public const int ControllerArea_RuntimeModified = 3737; - public const int UDT_Class_Rid = 3932; + public const int UDT_Class_Rid = 3932; public const int DAI_MessageType = 4079; - public const int ConstantsGlobal_Symbolics = 4275; - public const int ASObjectES_Comment = 4288; - public const int AlarmSubscriptionRef_AlarmDomain2 = 7731; - public const int DAI_HmiInfo = 7813; - public const int MultipleSTAI_Class_Rid = 7854; - public const int MultipleSTAI_STAIs = 7859; - public const int DAI_SequenceCounter = 7917; - public const int AlarmSubscriptionRef_AlarmTextLanguages_Rid = 8181; - public const int AlarmSubscriptionRef_SendAlarmTexts_Rid = 8173; - public const int ReturnValue = 40305; - public const int LID_LegitimationPayloadStruct = 40400; - public const int LID_LegitimationPayloadType = 40401; - public const int LID_LegitimationPayloadUsername = 40402; - public const int LID_LegitimationPayloadPassword = 40403; - - public const uint ReleaseMngmtRoot_Rid = 2303328256; - - public const uint Constants = 0x8a110000; - - public const int TI_BOOL = 0x02000000 + 1; - public const int TI_BYTE = 0x02000000 + 2; - public const int TI_CHAR = 0x02000000 + 3; - public const int TI_WORD = 0x02000000 + 4; - public const int TI_INT = 0x02000000 + 5; - public const int TI_DWORD = 0x02000000 + 6; - public const int TI_DINT = 0x02000000 + 7; - public const int TI_REAL = 0x02000000 + 8; - public const int TI_STRING = 0x02000000 + 19; - public const int TI_LREAL = 0x02000000 + 48; - public const int TI_USINT = 0x02000000 + 52; - public const int TI_UINT = 0x02000000 + 53; - public const int TI_UDINT = 0x02000000 + 54; - public const int TI_SINT = 0x02000000 + 55; - public const int TI_WCHAR = 0x02000000 + 61; - public const int TI_WSTRING = 0x02000000 + 62; - public const int TI_STRING_START = 0x020a0000; // Start for String[0] - public const int TI_STRING_END = 0x020affff; // End (String[65535]) - public const int TI_WSTRING_START = 0x020b0000; // Start for WString[0] - public const int TI_WSTRING_END = 0x020bffff; // End (WString[65535]) - } -} + public const int ConstantsGlobal_Symbolics = 4275; + public const int ASObjectES_Comment = 4288; + public const int AlarmSubscriptionRef_AlarmDomain2 = 7731; + public const int DAI_HmiInfo = 7813; + public const int MultipleSTAI_Class_Rid = 7854; + public const int MultipleSTAI_STAIs = 7859; + public const int DAI_SequenceCounter = 7917; + public const int AlarmSubscriptionRef_AlarmTextLanguages_Rid = 8181; + public const int AlarmSubscriptionRef_SendAlarmTexts_Rid = 8173; + public const int ReturnValue = 40305; + public const int LID_LegitimationPayloadStruct = 40400; + public const int LID_LegitimationPayloadType = 40401; + public const int LID_LegitimationPayloadUsername = 40402; + public const int LID_LegitimationPayloadPassword = 40403; + + public const uint ReleaseMngmtRoot_Rid = 2303328256; + + public const uint Constants = 0x8a110000; + + public const int TI_BOOL = 0x02000000 + 1; + public const int TI_BYTE = 0x02000000 + 2; + public const int TI_CHAR = 0x02000000 + 3; + public const int TI_WORD = 0x02000000 + 4; + public const int TI_INT = 0x02000000 + 5; + public const int TI_DWORD = 0x02000000 + 6; + public const int TI_DINT = 0x02000000 + 7; + public const int TI_REAL = 0x02000000 + 8; + public const int TI_STRING = 0x02000000 + 19; + public const int TI_LREAL = 0x02000000 + 48; + public const int TI_USINT = 0x02000000 + 52; + public const int TI_UINT = 0x02000000 + 53; + public const int TI_UDINT = 0x02000000 + 54; + public const int TI_SINT = 0x02000000 + 55; + public const int TI_WCHAR = 0x02000000 + 61; + public const int TI_WSTRING = 0x02000000 + 62; + public const int TI_STRING_START = 0x020a0000; // Start for String[0] + public const int TI_STRING_END = 0x020affff; // End (String[65535]) + public const int TI_WSTRING_START = 0x020b0000; // Start for WString[0] + public const int TI_WSTRING_END = 0x020bffff; // End (WString[65535]) + } +} diff --git a/src/S7CommPlusDriver/Core/PValue.cs b/src/S7CommPlusDriver/Core/PValue.cs index 68a8625..bb61e86 100644 --- a/src/S7CommPlusDriver/Core/PValue.cs +++ b/src/S7CommPlusDriver/Core/PValue.cs @@ -2387,7 +2387,7 @@ public class ValueRID : PValue { UInt32 Value; - public ValueRID(UInt32 rid) : this (rid, 0) + public ValueRID(UInt32 rid) : this(rid, 0) { } @@ -3118,7 +3118,7 @@ public enum PackedStructTransportFlagBits Count2Present = 1 << 10 // If this bit is set, then there's a 2nd counter present. Which if for a rare case you can read an array of struct, if the complete size, the 1st for one element. } - public ValueStruct(UInt32 value) : this (value, 0) + public ValueStruct(UInt32 value) : this(value, 0) { } @@ -3134,7 +3134,7 @@ public UInt32 GetValue() return Value; } - public void AddStructElement(uint id, PValue elem) + public void AddStructElement(uint id, PValue elem) { Elements.Add(id, elem); } diff --git a/src/S7CommPlusDriver/Exceptions.cs b/src/S7CommPlusDriver/Exceptions.cs new file mode 100644 index 0000000..3e2e05b --- /dev/null +++ b/src/S7CommPlusDriver/Exceptions.cs @@ -0,0 +1,46 @@ +using System; + +namespace S7CommPlusDriver +{ + public class ErrTCPConnectionFailed : Exception + { + public ErrTCPConnectionFailed(string location, string message) : base("TCP: Connection Error - " + message + " - " + location) + { + } + } + + public class ErrTCPDataSend : Exception + { + public ErrTCPDataSend(string location, string message) : base("TCP: Error sending data - " + message + " - " + location) + { + } + } + + public class ErrTCPNotConnected : Exception + { + public ErrTCPNotConnected(string location, string message) : base("CLI: Client not connected - " + message + " - " + location) + { + } + } + + public class ErrTCPDataReceive : Exception + { + public ErrTCPDataReceive(string location, string message) : base("TCP: Error receiving data - " + message + " - " + location) + { + } + } + + public class ErrIsoConnect : Exception + { + public ErrIsoConnect(string location, string message) : base("ISO: Connection Error - " + message + " - " + location) + { + } + } + + public class ErrIsoInvalidPDU : Exception + { + public ErrIsoInvalidPDU(string location, string message) : base("ISO: Invalid PDU received - " + message + " - " + location) + { + } + } +} \ No newline at end of file diff --git a/src/S7CommPlusDriver/Legitimation/Legitimation.cs b/src/S7CommPlusDriver/Legitimation/Legitimation.cs index 41e97c7..2597d7c 100644 --- a/src/S7CommPlusDriver/Legitimation/Legitimation.cs +++ b/src/S7CommPlusDriver/Legitimation/Legitimation.cs @@ -8,8 +8,9 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; -namespace S7CommPlusDriver { - public partial class S7CommPlusConnection +namespace S7CommPlusDriver +{ + public partial class S7CommPlusConnection { private byte[] omsSecret; @@ -23,23 +24,55 @@ public partial class S7CommPlusConnection /// error code (0 = ok) private int legitimate(ValueStruct serverSession, string password, string username = "") { + // S7-1214C (6ES7 214-1AG40-0XB0) 1;6ES7 214-1AG40-0XB0 ;V4.5 + // S7-1510SP (6ES7 510-1DJ01-0AB0) 1;6ES7 510-1DJ01-0AB0;V2.9 + // S7-1507SF (6ES7 672-7FC01-0YA0) 1;6ES7 672-7FC01-0YA0;V21.9 + // Parse device and firmware version + // doc: https://cache.industry.siemens.com/dl/files/068/109769068/att_1329908/v4/109769068_UsingCertificatesWithTIAPortal_DOC_V2_1_en.pdf + // Certificates in the scope of PG/PC and HMI communication + // Starting with TIA Portal V17, PG / PC and HMI communication is secured with TLS, protecting the data exchanged between + // Field PGs and HMIs with SIMATIC CPUs. + // The CPU families that support Secure PG / HMI communication are: + // • S7 - 1500 controllers as of firmware version V2.9. + // • S7 - 1200 controllers as of firmware version V4.5. + // • Software controllers as of firmware version V21.9. + // • SIMATIC Drive controllers as of firmware version V2.9. + // • PLCSim and PLCSim Advanced Version V4.0. + // HMI components that support Secure PG/ HMI communication, as of image version V17, are: + // • Panels or PCs configured with WinCC Basic, Comfort and Advanced. + // • PCs with WinCC RT Professional. + // • WinCC Unified PCs and Comfort Panels. + // In addition, SINAMICS RT SW, as of version V6.1, and STARTDRIVE, as of version V17, support secure communication string sessionVersionPAOMString = ((ValueWString)serverSession.GetStructElement((uint)Ids.LID_SessionVersionSystemPAOMString)).GetValue(); - Regex reVersions = new Regex("^.*;.*[17]\\s?([52]\\d\\d).+;[VS](\\d\\.\\d)$"); + var reVersions = new Regex( + @"^[^;]*;[^;]*[17]\s?(\d{3}).*;[VS](\d{1,2}\.\d+)$", + RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase + ); Match m = reVersions.Match(sessionVersionPAOMString); if (!m.Success) { Console.WriteLine("S7CommPlusConnection - Legitimate: Could not extract firmware version!"); return S7Consts.errCliFirmwareNotSupported; } - string deviceVersion = m.Groups[1].Value; - string firmwareVersion = m.Groups[2].Value; - int fwVerNo = int.Parse(firmwareVersion.Split('.')[0]) * 100; - fwVerNo += int.Parse(firmwareVersion.Split('.')[1]); + string deviceVersion = m.Groups[1].Value; // e.g., "672" + string firmwareVersion = m.Groups[2].Value; // e.g., "21.9" + + // Compute fwVerNo = major*100 + minor (e.g., "21.9" -> 2109) + int fwVerNo; + { + var parts = firmwareVersion.Split('.'); + if (parts.Length < 2 || !int.TryParse(parts[0], out var major) || !int.TryParse(parts[1], out var minor)) + { + Console.WriteLine($"S7CommPlusConnection - Legitimate: Invalid firmware format: {firmwareVersion}"); + return S7Consts.errCliFirmwareNotSupported; + } + fwVerNo = (major * 100) + minor; + } // Check if we have to use legacy legitimation via the firmware version bool legacyLegitimation = false; - if (deviceVersion.StartsWith("5")) + if (deviceVersion.StartsWith("5")) // S7-1500 (5xx) { if (fwVerNo < 209) { @@ -51,7 +84,11 @@ private int legitimate(ValueStruct serverSession, string password, string userna legacyLegitimation = true; } } - else if (deviceVersion.StartsWith("2")) + else if (sessionVersionPAOMString.Contains("50-0XB0") && deviceVersion.StartsWith("2")) //New S7-1200 G2 (example: "1;6ES7 212-1HG50-0XB0;V1.0") + { + legacyLegitimation = false; + } + else if (deviceVersion.StartsWith("2")) // S7-1200 (2xx) { if (fwVerNo < 403) { @@ -63,6 +100,15 @@ private int legitimate(ValueStruct serverSession, string password, string userna legacyLegitimation = true; } } + else if (deviceVersion.StartsWith("6")) // S7-1507S (6xx) + { + if (fwVerNo < 2109) + { + Console.WriteLine("S7CommPlusConnection - Legitimate: Firmware version is not supported!"); + return S7Consts.errCliFirmwareNotSupported; + } + legacyLegitimation = true; + } else { Console.WriteLine("S7CommPlusConnection - Legitimate: Device version is not supported!"); diff --git a/src/S7CommPlusDriver/Net/MsgSocket.cs b/src/S7CommPlusDriver/Net/MsgSocket.cs index 297bbde..bab09dc 100644 --- a/src/S7CommPlusDriver/Net/MsgSocket.cs +++ b/src/S7CommPlusDriver/Net/MsgSocket.cs @@ -8,19 +8,19 @@ #endregion using System; +using System.Diagnostics; using System.Net.Sockets; using System.Threading; namespace S7CommPlusDriver { - // - class MsgSocket + + class MsgSocket { private Socket TCPSocket; private int _ReadTimeout = 2000; private int _WriteTimeout = 2000; private int _ConnectTimeout = 1000; - public int LastError = 0; public MsgSocket() { @@ -40,18 +40,12 @@ public void Close() } } - private void CreateSocket() - { - TCPSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - TCPSocket.NoDelay = true; - } private void TCPPing(string Host, int Port) { // To Ping the PLC an Asynchronous socket is used rather then an ICMP packet. // This allows the use also across Internet and Firewalls (obviously the port must be opened) - LastError = 0; - Socket PingSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + Socket PingSocket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { IAsyncResult result = PingSocket.BeginConnect(Host, Port, null, null); @@ -59,43 +53,52 @@ private void TCPPing(string Host, int Port) if (!success) { - LastError = S7Consts.errTCPConnectionFailed; + throw new ErrTCPConnectionFailed("MsgSocket->TCPPing", "Connection to ping socket failed"); } } - catch + catch (Exception e) when (e is not ErrTCPConnectionFailed) { - LastError = S7Consts.errTCPConnectionFailed; - }; + PingSocket.Close(); + throw new ErrTCPConnectionFailed("MsgSocket->TCPPing", "Connection to ping socket failed: " + e.Message); + } + PingSocket.Close(); } - public int Connect(string Host, int Port) + public void Connect(string Host, int Port) { - LastError = 0; if (!Connected) { // TWI: TCPPing rausgenommen, stört bei Wireshark Analyse - //TCPPing(Host, Port); - if (LastError == 0) - try - { - CreateSocket(); - TCPSocket.Connect(Host, Port); - } - catch + // TCPPing(Host, Port); + try + { + TCPSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) { - LastError = S7Consts.errTCPConnectionFailed; - } + NoDelay = true + }; + } + catch (Exception e) + { + throw new ErrTCPConnectionFailed("MsgSocket->Connect", "Error creating socket: " + e.Message); + } + + try + { + TCPSocket.Connect(Host, Port); + } + catch (Exception e) + { + throw new ErrTCPConnectionFailed("MsgSocket->Connect", "Error connecting to socket: " + e.Message); + } } - return LastError; } - private int WaitForData(int Size, int Timeout) + private void WaitForData(int Size, int Timeout) { bool Expired = false; int SizeAvail; - int Elapsed = Environment.TickCount; - LastError = 0; + Stopwatch Elapsed = Stopwatch.StartNew(); try { SizeAvail = TCPSocket.Available; @@ -103,7 +106,7 @@ private int WaitForData(int Size, int Timeout) { Thread.Sleep(2); SizeAvail = TCPSocket.Available; - Expired = Environment.TickCount - Elapsed > Timeout; + Expired = Elapsed.ElapsedMilliseconds > Timeout; // If timeout we clean the buffer if (Expired && (SizeAvail > 0)) try @@ -114,61 +117,54 @@ private int WaitForData(int Size, int Timeout) catch { } } } - catch + catch (Exception e) { - LastError = S7Consts.errTCPDataReceive; + throw new ErrTCPDataReceive("MsgSocket->WaitForData", "Problem while waiting for data: " + e.Message); } if (Expired) { - LastError = S7Consts.errTCPDataReceive; + throw new ErrTCPDataReceive("MsgSocket->WaitForData", "Data expired."); } - return LastError; } - public int Receive(byte[] Buffer, int Start, int Size) + public void Receive(byte[] Buffer, int Start, int Size) { - - int BytesRead = 0; - LastError = WaitForData(Size, _ReadTimeout); - if (LastError == 0) + int BytesRead; + WaitForData(Size, _ReadTimeout); + try { - try - { - BytesRead = TCPSocket.Receive(Buffer, Start, Size, SocketFlags.None); - } - catch - { - LastError = S7Consts.errTCPDataReceive; - } - if (BytesRead == 0) // Connection Reset by the peer - { - LastError = S7Consts.errTCPDataReceive; - Close(); - } + BytesRead = TCPSocket.Receive(Buffer, Start, Size, SocketFlags.None); + } + catch (Exception e) + { + throw new ErrTCPDataReceive("MsgSocket->Receive", "Problem receiving data: " + e.Message); + } + if (BytesRead == 0) // Connection Reset by the peer + { + Close(); + throw new ErrTCPDataReceive("MsgSocket->Receive", "Connection reset by the peer"); } - return LastError; } - public int Send(byte[] Buffer, int Size) + public void Send(byte[] Buffer, int Size) { - LastError = 0; try { - int BytesSent = TCPSocket.Send(Buffer, Size, SocketFlags.None); + // int BytesSent = + TCPSocket.Send(Buffer, Size, SocketFlags.None); } - catch + catch (Exception e) { - LastError = S7Consts.errTCPDataSend; Close(); + throw new ErrTCPDataSend("MsgSocket->Send", "Problem sending data: " + e.Message); } - return LastError; } public bool Connected { get { - return (TCPSocket != null) && (TCPSocket.Connected); + return (TCPSocket != null) && TCPSocket.Connected; } } diff --git a/src/S7CommPlusDriver/Net/S7Client.cs b/src/S7CommPlusDriver/Net/S7Client.cs index 28fa468..bd84cc4 100644 --- a/src/S7CommPlusDriver/Net/S7Client.cs +++ b/src/S7CommPlusDriver/Net/S7Client.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -9,6 +9,7 @@ using OpenSsl; using System; +using System.Diagnostics; using System.IO; using System.Threading; @@ -22,14 +23,8 @@ namespace S7CommPlusDriver public class S7Client : OpenSSLConnector.IConnectorCallback { //TODO: better API, maybe a Callback - public static bool WriteSslKeyToFile; - public static string WriteSslKeyPath; - - #region [Constants and TypeDefs] - - public int _LastError = 0; - - #endregion + public static bool WriteSslKeyToFile; + public static string WriteSslKeyPath; #region [S7 Telegrams] @@ -111,7 +106,7 @@ public void OnDataAvailable() public void SSL_CTX_keylog_cb(IntPtr ssl, string line) { string filename = "key_" + m_DateTimeStarted.ToString("yyyyMMdd_HHmmss") + ".log"; - if (WriteSslKeyPath != null) + if (WriteSslKeyPath != null) filename = Path.Combine(WriteSslKeyPath, filename); StreamWriter file = new StreamWriter(filename, append: true); file.WriteLine(line); @@ -125,7 +120,7 @@ public int SslActivate() try { ret = Native.OPENSSL_init_ssl(0, IntPtr.Zero); // returns 1 on success or 0 on error - if (ret != 1) + if (ret != 1) { return S7Consts.errOpenSSL; } @@ -142,17 +137,17 @@ public int SslActivate() return S7Consts.errOpenSSL; } m_sslconn = new OpenSSLConnector(m_ptr_ctx, this); - m_sslconn.ExpectConnect(); - - // Keylog callback setzen - if (WriteSslKeyToFile) - { - m_keylog_cb = new Native.SSL_CTX_keylog_cb_func(SSL_CTX_keylog_cb); - Native.SSL_CTX_set_keylog_callback(m_ptr_ctx, m_keylog_cb); + m_sslconn.ExpectConnect(); + + // Keylog callback setzen + if (WriteSslKeyToFile) + { + m_keylog_cb = new Native.SSL_CTX_keylog_cb_func(SSL_CTX_keylog_cb); + Native.SSL_CTX_set_keylog_callback(m_ptr_ctx, m_keylog_cb); } m_SslActive = true; - } + } catch { return S7Consts.errOpenSSL; @@ -182,10 +177,17 @@ private void RunThread() while (!m_runThread_DoStop) { // Versuchen zu lesen - _LastError = 0; - Length = RecvIsoPacket(); + try + { + Length = RecvIsoPacket(); + } + catch + { + Length = 0; + } // TODO: Hier nur den Payload zurückgeben - if (Length > 0) { + if (Length > 0) + { byte[] Buffer = new byte[Length - TPKT_ISO.Length]; Array.Copy(PDU, TPKT_ISO.Length, Buffer, 0, Length - TPKT_ISO.Length); int Size = Length - TPKT_ISO.Length; @@ -193,7 +195,9 @@ private void RunThread() { // Durch SSL eingelesene Daten an SSL weiterleiten m_sslconn.ReadCompleted(Buffer, Size); - } else { + } + else + { // Wenn etwas gelesen werden konnte, Client benachrichtigen OnDataReceived?.Invoke(Buffer, Size); } @@ -229,7 +233,7 @@ private void RunThread() private byte LastPDUType; private byte[] PDU = new byte[2048]; private MsgSocket Socket = null; - private int Time_ms = 0; + private long Time_ms = 0; private void CreateSocket() { @@ -245,31 +249,45 @@ private void CreateSocket() } } - private int TCPConnect() + private void TCPConnect() { - if (_LastError == 0) - try - { - _LastError = Socket.Connect(IPAddress, _PLCPort); - } - catch - { - _LastError = S7Consts.errTCPConnectionFailed; - } - return _LastError; + try + { + Socket.Connect(IPAddress, _PLCPort); + } + catch (Exception e) when (e is not ErrTCPConnectionFailed) + { + throw new ErrTCPConnectionFailed("S7Client->TCPConnect", "Problem connecting to socket: " + e.Message); + } } private void RecvPacket(byte[] Buffer, int Start, int Size) { if (Connected) - _LastError = Socket.Receive(Buffer, Start, Size); + { + try + { + Socket.Receive(Buffer, Start, Size); + } + catch (Exception e) + { + throw new ErrTCPDataReceive("S7Client->RecvPacket", "Problem while receiving packets: " + e.Message); + } + } else - _LastError = S7Consts.errTCPNotConnected; + throw new ErrTCPNotConnected("S7Client->RecvPacket", "S7Client is not connected"); } private void SendPacket(byte[] Buffer, int Len) { - _LastError = Socket.Send(Buffer, Len); + try + { + Socket.Send(Buffer, Len); + } + catch (Exception e) + { + throw new ErrTCPDataSend("S7Client->SendPacket", "Problem while sending packets: " + e.Message); + } } private void SendPacket(byte[] Buffer) @@ -277,7 +295,7 @@ private void SendPacket(byte[] Buffer) if (Connected) SendPacket(Buffer, Buffer.Length); else - _LastError = S7Consts.errTCPNotConnected; + throw new ErrTCPNotConnected("S7Client->SendPacket", "S7Client is not connected"); } public void Send(byte[] Buffer) @@ -292,11 +310,10 @@ public void Send(byte[] Buffer) } } - private int SendIsoPacket(byte[] Buffer) + private void SendIsoPacket(byte[] Buffer) { // Packt die zu sendenden Daten in den Iso-Header ein. int Size = Buffer.Length; - _LastError = 0; Array.Copy(TPKT_ISO, 0, PDU, 0, TPKT_ISO.Length); SetWordAt(PDU, 2, (ushort)(Size + TPKT_ISO.Length)); @@ -304,13 +321,12 @@ private int SendIsoPacket(byte[] Buffer) { Array.Copy(Buffer, 0, PDU, TPKT_ISO.Length, Size); } - catch + catch (Exception e) { - return S7Consts.errIsoInvalidPDU; + throw new ErrIsoInvalidPDU("S7Client->SendIsoPacket", "Problem copying buffer: " + e.Message); } - SendPacket(PDU, TPKT_ISO.Length + Size); - return _LastError; + SendPacket(PDU, TPKT_ISO.Length + Size); } private UInt16 GetWordAt(byte[] Buffer, int Pos) @@ -326,42 +342,33 @@ private void SetWordAt(byte[] Buffer, int Pos, UInt16 Value) private int RecvIsoPacket() { - Boolean Done = false; + bool Done = false; int Size = 0; - while ((_LastError == 0) && !Done) + while (!Done) { // Get TPKT (4 bytes) RecvPacket(PDU, 0, 4); - if (_LastError == 0) + Size = GetWordAt(PDU, 2); + // Check 0 bytes Data Packet (only TPKT+COTP = 7 bytes) + if (Size == IsoHSize) + RecvPacket(PDU, 4, 3); // Skip remaining 3 bytes and Done is still false + else { - Size = GetWordAt(PDU, 2); - // Check 0 bytes Data Packet (only TPKT+COTP = 7 bytes) - if (Size == IsoHSize) - RecvPacket(PDU, 4, 3); // Skip remaining 3 bytes and Done is still false - else - { - // TODO: Größe korrekt prüfen - //if ((Size > _PduSizeRequested + IsoHSize) || (Size < MinPduSize)) - // _LastError = S7Consts.errIsoInvalidPDU; - //else - Done = true; // a valid Length !=7 && >16 && <247 - } + // TODO: Größe korrekt prüfen + //if ((Size > _PduSizeRequested + IsoHSize) || (Size < MinPduSize)) + // _LastError = S7Consts.errIsoInvalidPDU; + //else + Done = true; // a valid Length !=7 && >16 && <247 } } - if (_LastError == 0) - { - RecvPacket(PDU, 4, 3); // Skip remaining 3 COTP bytes - LastPDUType = PDU[5]; // Stores PDU Type, we need it - // Receives the S7 Payload - RecvPacket(PDU, 7, Size - IsoHSize); - } - if (_LastError == 0) - return Size; - else - return 0; + RecvPacket(PDU, 4, 3); // Skip remaining 3 COTP bytes + LastPDUType = PDU[5]; // Stores PDU Type, we need it + // Receives the S7 Payload + RecvPacket(PDU, 7, Size - IsoHSize); + return Size; } - private int ISOConnect() + private void ISOConnect() { int Size; byte[] isocon = new byte[ISO_CR.Length + RemoteTSAP_S.Length]; @@ -377,22 +384,15 @@ private int ISOConnect() // Sends the connection request telegram SendPacket(isocon); - if (_LastError == 0) + // Gets the reply (if any) + Size = RecvIsoPacket(); + if (Size == 36) { - // Gets the reply (if any) - Size = RecvIsoPacket(); - if (_LastError == 0) - { - if (Size == 36) - { - if (LastPDUType != (byte)0xD0) // 0xD0 = CC Connection confirm - _LastError = S7Consts.errIsoConnect; - } - else - _LastError = S7Consts.errIsoInvalidPDU; - } + if (LastPDUType != (byte)0xD0) // 0xD0 = CC Connection confirm + throw new ErrIsoConnect("S7Client->ISOConnect", "Didn't receive CC byte"); } - return _LastError; + else + throw new ErrIsoInvalidPDU("S7Client->ISOConnect", "Wrong packet size"); } public byte[] getOMSExporterSecret() @@ -416,30 +416,27 @@ public S7Client() Disconnect(); } - public int Connect() + public void Connect() { - _LastError = 0; Time_ms = 0; - int Elapsed = Environment.TickCount; + Stopwatch Elapsed = Stopwatch.StartNew(); if (!Connected) { - TCPConnect(); // First stage : TCP Connection - if (_LastError == 0) + try { + TCPConnect(); // First stage : TCP Connection ISOConnect(); // Second stage : ISOTCP (ISO 8073) Connection - if (_LastError == 0) - { - // _LastError = S7P_InitSSLRequest(); // Third stage : Init SSL Request - StartThread(); - } + // _LastError = S7P_InitSSLRequest(); // Third stage : Init SSL Request + StartThread(); + } + catch + { + Disconnect(); + throw; } } - if (_LastError != 0) - Disconnect(); else - Time_ms = Environment.TickCount - Elapsed; - - return _LastError; + Time_ms = Elapsed.ElapsedMilliseconds; } public int SetConnectionParams(string Address, ushort LocalTSAP, byte[] RemoteTSAP) @@ -455,14 +452,12 @@ public int SetConnectionParams(string Address, ushort LocalTSAP, byte[] RemoteTS return 0; } - public int Disconnect() + public void Disconnect() { m_runThread_DoStop = true; m_runThread?.Join(); Socket.Close(); - - return 0; } public int GetParam(Int32 ParamNumber, ref int Value) @@ -553,17 +548,17 @@ public static string ErrorText(int Error) switch (Error) { case 0: return "OK"; - case S7Consts.errTCPSocketCreation: return "SYS : Error creating the Socket"; - case S7Consts.errTCPConnectionTimeout: return "TCP : Connection Timeout"; - case S7Consts.errTCPConnectionFailed: return "TCP : Connection Error"; - case S7Consts.errTCPReceiveTimeout: return "TCP : Data receive Timeout"; + // case S7Consts.errTCPSocketCreation: return "SYS : Error creating the Socket"; + // case S7Consts.errTCPConnectionTimeout: return "TCP : Connection Timeout"; + // case S7Consts.errTCPConnectionFailed: return "TCP : Connection Error"; + // case S7Consts.errTCPReceiveTimeout: return "TCP : Data receive Timeout"; case S7Consts.errTCPDataReceive: return "TCP : Error receiving Data"; - case S7Consts.errTCPSendTimeout: return "TCP : Data send Timeout"; - case S7Consts.errTCPDataSend: return "TCP : Error sending Data"; - case S7Consts.errTCPConnectionReset: return "TCP : Connection reset by the Peer"; - case S7Consts.errTCPNotConnected: return "CLI : Client not connected"; - case S7Consts.errTCPUnreachableHost: return "TCP : Unreachable host"; - case S7Consts.errIsoConnect: return "ISO : Connection Error"; + // case S7Consts.errTCPSendTimeout: return "TCP : Data send Timeout"; + // case S7Consts.errTCPDataSend: return "TCP : Error sending Data"; + // case S7Consts.errTCPConnectionReset: return "TCP : Connection reset by the Peer"; + // case S7Consts.errTCPNotConnected: return "CLI : Client not connected"; + // case S7Consts.errTCPUnreachableHost: return "TCP : Unreachable host"; + // case S7Consts.errIsoConnect: return "ISO : Connection Error"; case S7Consts.errIsoInvalidPDU: return "ISO : Invalid PDU received"; case S7Consts.errIsoInvalidDataSize: return "ISO : Invalid Buffer passed to Send/Receive"; case S7Consts.errCliNegotiatingPDU: return "CLI : Error in PDU negotiation"; @@ -606,12 +601,7 @@ public static string ErrorText(int Error) case S7Consts.errCliFirmwareNotSupported: return "CLI : Firmware not supported"; case S7Consts.errCliDeviceNotSupported: return "CLI : Device type not supported"; default: return "CLI : Unknown error (0x" + Convert.ToString(Error, 16) + ")"; - }; - } - - public int LastError() - { - return _LastError; + } } public int RequestedPduLength() @@ -624,12 +614,12 @@ public int NegotiatedPduLength() return _PDULength; } - public int ExecTime() + public long ExecTime() { return Time_ms; } - public int ExecutionTime + public long ExecutionTime { get { @@ -713,7 +703,7 @@ public bool Connected { get { - return (Socket != null) && (Socket.Connected); + return (Socket != null) && Socket.Connected; } } #endregion diff --git a/src/S7CommPlusDriver/Net/S7Consts.cs b/src/S7CommPlusDriver/Net/S7Consts.cs index b8618d1..3871d40 100644 --- a/src/S7CommPlusDriver/Net/S7Consts.cs +++ b/src/S7CommPlusDriver/Net/S7Consts.cs @@ -22,31 +22,31 @@ public static class S7Consts //------------------------------------------------------------------------------ // ERRORS //------------------------------------------------------------------------------ - public const int errTCPSocketCreation = 0x00000001; - public const int errTCPConnectionTimeout = 0x00000002; - public const int errTCPConnectionFailed = 0x00000003; - public const int errTCPReceiveTimeout = 0x00000004; + // public const int errTCPSocketCreation = 0x00000001; + // public const int errTCPConnectionTimeout = 0x00000002; + // public const int errTCPConnectionFailed = 0x00000003; + // public const int errTCPReceiveTimeout = 0x00000004; public const int errTCPDataReceive = 0x00000005; - public const int errTCPSendTimeout = 0x00000006; - public const int errTCPDataSend = 0x00000007; - public const int errTCPConnectionReset = 0x00000008; - public const int errTCPNotConnected = 0x00000009; - public const int errTCPUnreachableHost = 0x00002751; + // public const int errTCPSendTimeout = 0x00000006; + // public const int errTCPDataSend = 0x00000007; + // public const int errTCPConnectionReset = 0x00000008; + // public const int errTCPNotConnected = 0x00000009; + // public const int errTCPUnreachableHost = 0x00002751; - public const int errIsoConnect = 0x00010000; // Connection error - public const int errIsoInvalidPDU = 0x00030000; // Bad format - public const int errIsoInvalidPDU1 = 0x00030001; // Bad format - public const int errIsoInvalidPDU2 = 0x00030002; // Bad format - public const int errIsoInvalidPDU3 = 0x00030003; // Bad format - public const int errIsoInvalidPDU4 = 0x00030004; // Bad format - public const int errIsoInvalidPDU5 = 0x00030005; // Bad format - public const int errIsoInvalidPDU6 = 0x00030006; // Bad format - public const int errIsoInvalidPDU7 = 0x00030007; // Bad format - public const int errIsoInvalidPDU8 = 0x00030008; // Bad format - public const int errIsoInvalidPDU9 = 0x00030009; // Bad format - public const int errIsoInvalidPDU10 = 0x00030010; // Bad format - public const int errIsoInvalidPDU11 = 0x00030011; // Bad format - public const int errIsoInvalidPDU12 = 0x00030012; // Bad format + // public const int errIsoConnect = 0x00010000; // Connection error + public const int errIsoInvalidPDU = 0x00030000; // Bad format + public const int errIsoInvalidPDU1 = 0x00030001; // Bad format + public const int errIsoInvalidPDU2 = 0x00030002; // Bad format + public const int errIsoInvalidPDU3 = 0x00030003; // Bad format + public const int errIsoInvalidPDU4 = 0x00030004; // Bad format + public const int errIsoInvalidPDU5 = 0x00030005; // Bad format + public const int errIsoInvalidPDU6 = 0x00030006; // Bad format + public const int errIsoInvalidPDU7 = 0x00030007; // Bad format + public const int errIsoInvalidPDU8 = 0x00030008; // Bad format + public const int errIsoInvalidPDU9 = 0x00030009; // Bad format + public const int errIsoInvalidPDU10 = 0x00030010; // Bad format + public const int errIsoInvalidPDU11 = 0x00030011; // Bad format + public const int errIsoInvalidPDU12 = 0x00030012; // Bad format public const int errIsoInvalidDataSize = 0x00040000; // Bad Datasize passed to send/recv : buffer is invalid public const int errCliNegotiatingPDU = 0x00100000; @@ -89,11 +89,11 @@ public static class S7Consts public const int errCliFirmwareNotSupported = 0x02800000; public const int errCliDeviceNotSupported = 0x02900000; - public const int errOpenSSL = 0x03100000; - public const int errInitSslResponse = 0x03100001; - //------------------------------------------------------------------------------ - // PARAMS LIST FOR COMPATIBILITY WITH Snap7.net.cs - //------------------------------------------------------------------------------ + public const int errOpenSSL = 0x03100000; + public const int errInitSslResponse = 0x03100001; + //------------------------------------------------------------------------------ + // PARAMS LIST FOR COMPATIBILITY WITH Snap7.net.cs + //------------------------------------------------------------------------------ public const Int32 p_u16_LocalPort = 1; // Not applicable here public const Int32 p_u16_RemotePort = 2; public const Int32 p_i32_PingTimeout = 3; diff --git a/src/S7CommPlusDriver/OpenSSL/OpenSSLConnector.cs b/src/S7CommPlusDriver/OpenSSL/OpenSSLConnector.cs index bf61865..1819e8c 100644 --- a/src/S7CommPlusDriver/OpenSSL/OpenSSLConnector.cs +++ b/src/S7CommPlusDriver/OpenSSL/OpenSSLConnector.cs @@ -218,7 +218,7 @@ protected void RunSSL() } } - public void Write(byte[] pData,int dataLen) + public void Write(byte[] pData, int dataLen) { DataBuffer pBuffer = new DataBuffer(pData, dataLen); AppendBuffer(m_pendingWriteList, pBuffer); diff --git a/src/S7CommPlusDriver/S7CommPlusConnection.cs b/src/S7CommPlusDriver/S7CommPlusConnection.cs index 25737f8..9c489df 100644 --- a/src/S7CommPlusDriver/S7CommPlusConnection.cs +++ b/src/S7CommPlusDriver/S7CommPlusConnection.cs @@ -1,1422 +1,1441 @@ -#region License -/****************************************************************************** - * S7CommPlusDriver - * - * Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de - * - * This file is part of S7CommPlusDriver. - * - * S7CommPlusDriver is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - /****************************************************************************/ -#endregion - -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; -using System.IO; -using System.Linq; -using System.Diagnostics; -using S7CommPlusDriver.ClientApi; -using System.Text.RegularExpressions; -using System.Security.Cryptography; - -namespace S7CommPlusDriver -{ - public partial class S7CommPlusConnection - { - #region Private Members - private S7Client m_client; - private MemoryStream m_ReceivedPDU; - private MemoryStream m_ReceivedTempPDU; - private Queue m_ReceivedPDUs = new Queue(); - private Mutex m_Mutex = new Mutex(); - - private bool m_ReceivedNeedMoreDataForCompletePDU; - private bool m_NewS7CommPlusReceived; - private UInt32 m_SessionId; - private UInt32 m_SessionId2; - public UInt32 SessionId2 - { - get { return m_SessionId2; } - private set { m_SessionId2 = value; } - } - - private int m_ReadTimeout = 5000; - private UInt16 m_SequenceNumber = 0; - private UInt32 m_IntegrityId = 0; - private UInt32 m_IntegrityId_Set = 0; - private CommRessources m_CommRessources = new CommRessources(); - - private List dbInfoList; - private List typeInfoList = new List(); - #endregion - - #region Public Members - public int m_LastError = 0; - - #endregion - - #region Private Methods - - private UInt16 GetNextSequenceNumber() - { - if (m_SequenceNumber == UInt16.MaxValue) - { - m_SequenceNumber = 1; - } - else - { - m_SequenceNumber++; - } - return m_SequenceNumber; - } - - // We must count the IntegrityId for different functions of the protocol. - // As a first guess functions for setting variables need separate counters. - // Use the functioncode to differ between the which sequence/integrity counter values. - private UInt32 GetNextIntegrityId(ushort functioncode) - { - UInt32 ret; - switch (functioncode) - { - case Functioncode.SetMultiVariables: - case Functioncode.SetVariable: - case Functioncode.SetVarSubStreamed: - case Functioncode.DeleteObject: - case Functioncode.CreateObject: - if (m_IntegrityId_Set == UInt32.MaxValue) - { - m_IntegrityId_Set = 0; - } - else - { - m_IntegrityId_Set++; - } - ret = m_IntegrityId_Set; - break; - default: - if (m_IntegrityId == UInt32.MaxValue) - { - m_IntegrityId = 0; - } - else - { - m_IntegrityId++; - } - ret = m_IntegrityId; - break; - } - return ret; - } - - private void WaitForNewS7plusReceived(int Timeout) - { - //TODO: Tickcount overflows!!!! - bool Expired = false; - int Elapsed = Environment.TickCount; - bool done = false; - - m_Mutex.WaitOne(); - if (m_ReceivedPDUs.Count > 0) - { - m_ReceivedPDU = m_ReceivedPDUs.Dequeue(); - done = true; - } - m_Mutex.ReleaseMutex(); - - while (!done && !Expired) - { - Thread.Sleep(2); - Expired = Environment.TickCount - Elapsed > Timeout; - m_Mutex.WaitOne(); - if (m_ReceivedPDUs.Count > 0) - { - m_ReceivedPDU = m_ReceivedPDUs.Dequeue(); - done = true; - } - m_Mutex.ReleaseMutex(); - } - - if (Expired) - { - Console.WriteLine("S7CommPlusConnection - WaitForNewS7plusReceived: ERROR: Timeout!"); - m_LastError = S7Consts.errTCPDataReceive; - } - } - - private int SendS7plusFunctionObject(IS7pRequest funcObj) - { - // If we don't have a SessionId, this must be the first CreateObjectRequest, where we use the Id for NullServerSession - if (m_SessionId == 0) - { - funcObj.SessionId = Ids.ObjectNullServerSession; - } - else - { - funcObj.SessionId = m_SessionId; - } - - // Insert SequenceNumber and IntegrityId, if neccessary for object type and state of communication - funcObj.SequenceNumber = GetNextSequenceNumber(); - if (funcObj.WithIntegrityId) - { - funcObj.IntegrityId = GetNextIntegrityId(funcObj.FunctionCode); - } - - MemoryStream stream = new MemoryStream(); - funcObj.Serialize(stream); - return SendS7plusPDUdata(stream.ToArray(), (int)stream.Length, funcObj.ProtocolVersion); - } - - private int SendS7plusPDUdata(byte[] sendPduData, int bytesToSend, byte protoVersion) - { - m_LastError = 0; - - int curSize; - int sourcePos = 0; - int sendLen; - int NegotiatedIsoPduSize = 1024;// TODO: Respect the negotiated TPDU size - - // 4 Byte TPKT Header - // 3 Byte ISO-Header - // 5 Byte TLS Header + 17 Bytes addition from TLS - // 4 Byte S7CommPlus Header - // 4 Byte S7CommPlus Trailer (must fit into last PDU) - int MaxSize = NegotiatedIsoPduSize - 4 - 3 - 5 - 17 - 4 - 4; - byte[] packet = new byte[MaxSize + 4]; //max packet size is always MaxSize + PDU Header - - while (bytesToSend > 0) - { - if (bytesToSend > MaxSize) - { - curSize = MaxSize; - bytesToSend -= MaxSize; - } - else - { - curSize = bytesToSend; - bytesToSend -= curSize; - } - // Header - packet[0] = 0x72; - packet[1] = protoVersion; - packet[2] = (byte)(curSize >> 8); - packet[3] = (byte)(curSize & 0x00FF); - // Data part - Array.Copy(sendPduData, sourcePos, packet, 4, curSize); - sourcePos += curSize; - sendLen = 4 + curSize; - - // Trailer only in last packet - if (bytesToSend == 0) - { - Array.Resize(ref packet, sendLen + 4); //resize only the last package to sendLen + TrailerLen - packet[sendLen] = 0x72; - sendLen++; - packet[sendLen] = protoVersion; - sendLen++; - packet[sendLen] = 0; - sendLen++; - packet[sendLen] = 0; - sendLen++; - } - m_client.Send(packet); - } - return m_LastError; - } - - private void OnDataReceived(byte[] PDU, int len) - { - // In this method, we've got always a complete TPDU (from protocol layer above) without fragmentation - // At this point, we can detect if we receive a fragmented S7CommPlus PDU. - // If not fragmented, then TPKT.Length - 15 is equal of the length in S7CommPlus.Header. - // 15 bytes because: 4 Bytes TPKT.Header.len + 3 Bytes ISO.Header.Len + 4 Bytes S7CommPlus.Header.len + 4 Bytes S7CommPlus.trailer.Len. - // Since the pure userdata of the TPDU comes in here, that is only minus 4 bytes header + 4 bytes trailer. - // - // Special handling for SystemEvents with ProtocolVersion = 0xfe: - // Here's only a header. - // Because of this, the first byte for the ProtocolVersion must be written in then stream at first. - // The datalength must not be written into the stream, because it's not valid on fragmented PDUs - // for the complete length, only for the single fragment. - - // This method is called from a different thread. - // If we use subscriptions or alarming, we may get new data before the last PDU was processed completely. - // First step we push the complete PDU to a queue. - // TODO: m_LastError handling would also not work as expected. This needs some more redesign. - - if (!m_ReceivedNeedMoreDataForCompletePDU) - { - m_ReceivedTempPDU = new MemoryStream(); - } - // S7comm-plus - byte protoVersion; - int pos = 0; - int s7HeaderDataLen = 0; - // Check header - if (PDU[pos] != 0x72) - { - m_ReceivedNeedMoreDataForCompletePDU = false; - m_LastError = S7Consts.errIsoInvalidPDU1; - return; - } - pos++; - protoVersion = PDU[pos]; - if (protoVersion != ProtocolVersion.V1 && protoVersion != ProtocolVersion.V2 && protoVersion != ProtocolVersion.V3 && protoVersion != ProtocolVersion.SystemEvent) - { - m_ReceivedNeedMoreDataForCompletePDU = false; - m_LastError = S7Consts.errIsoInvalidPDU2; - return; - } - // For the first fragment, write the ProtocolVersion into the stream in advance - if (!m_ReceivedNeedMoreDataForCompletePDU) - { - m_ReceivedTempPDU.Write(PDU, pos, 1); - } - pos++; - - // Read the length of the data-part from header - s7HeaderDataLen = GetWordAt(PDU, pos); - pos += 2; - if (s7HeaderDataLen > 0) - { - // Special handling for SystemEvent 0xfe PDUs: - // This only confirms a few data, but also reports major protocol errors (e.g. incorrect sequence numbers). - // The confirms can be discarded (for now), but the errors are relevant, because a connection termination is neccessary. - // As we don't have a trailer on this types, it's not possible that they are transmitted as fragments. - if (protoVersion == ProtocolVersion.SystemEvent) - { - Console.WriteLine("S7CommPlusConnection - OnDataReceived: ProtocolVersion 0xfe SystemEvent received"); - m_ReceivedTempPDU.Write(PDU, pos, s7HeaderDataLen); - pos += s7HeaderDataLen; - // Create SystemEventObject - m_ReceivedNeedMoreDataForCompletePDU = false; - m_ReceivedTempPDU.Position = 0; - m_NewS7CommPlusReceived = false; - - var sysevt = SystemEvent.DeserializeFromPdu(m_ReceivedTempPDU); - if (sysevt.IsFatalError()) - { - Console.WriteLine("S7CommPlusConnection - OnDataReceived: SystemEvent has fatal error"); - // Termination neccessary - m_LastError = S7Consts.errIsoInvalidPDU3; - } - else - { - Console.WriteLine("S7CommPlusConnection - OnDataReceived: SystemEvent with non fatal error, do nothing"); - } - } - else - { - // Copy data part to destination stream - m_ReceivedTempPDU.Write(PDU, pos, s7HeaderDataLen); - pos += s7HeaderDataLen; - // If this is a fragmented PDU, then at this point no trailer - if ((len - 4 - 4) == s7HeaderDataLen) - { - m_ReceivedNeedMoreDataForCompletePDU = false; - m_ReceivedTempPDU.Position = 0; // Set position back to zero, ready for readout - m_NewS7CommPlusReceived = true; - } - else - { - m_ReceivedNeedMoreDataForCompletePDU = true; - } - } - } - - // If a complete (usable) PDU is received, add to the queue (threadsafe) for readout - if (m_NewS7CommPlusReceived) - { - // Push complete PDU to the queue - m_Mutex.WaitOne(); - m_ReceivedPDUs.Enqueue(m_ReceivedTempPDU); - m_Mutex.ReleaseMutex(); - m_NewS7CommPlusReceived = false; - } - } - - private UInt16 GetWordAt(byte[] Buffer, int Pos) - { - return (UInt16)((Buffer[Pos] << 8) | Buffer[Pos + 1]); - } - - private void SetWordAt(byte[] Buffer, int Pos, UInt16 Value) - { - Buffer[Pos] = (byte)(Value >> 8); - Buffer[Pos + 1] = (byte)(Value & 0x00FF); - } - - private void printBuf(byte[] b) - { - for (int i = 0; i < b.Length; i++) - { - Console.Write("0x" + String.Format("{0:X02} ", b[i])); - } - Console.Write(Environment.NewLine); - } - - private int checkResponseWithIntegrity(IS7pRequest request, IS7pResponse response) - { - if (response == null) - { - //Console.WriteLine("checkResponseWithIntegrity: ERROR! response == null"); - return S7Consts.errIsoInvalidPDU4; - } - if (request.SequenceNumber != response.SequenceNumber) - { - //Console.WriteLine(String.Format("checkResponseWithIntegrity: ERROR! SeqenceNumber of Response ({0}) doesn't match Request ({1})", response.SequenceNumber, request.SequenceNumber)); - return S7Consts.errIsoInvalidPDU5; - } - // Overflow is possible and allowed - UInt32 reqIntegCheck = (UInt32)request.SequenceNumber + request.IntegrityId; - if (response.IntegrityId != reqIntegCheck) - { - Console.WriteLine(String.Format("checkResponseWithIntegrity: ERROR! IntegrityId of the Response ({0}) doesn't match Request ({1})", response.IntegrityId, reqIntegCheck)); - // Don't return this as error so far - } - return 0; - } - #endregion - - #region Public Methods - /// - /// Establishes a connection to the PLC. - /// - /// PLC IP address - /// PLC password (if set) - /// read timeout in milliseconds (default: 5000 ms) - /// - public int Connect(string address, string password = "", string username = "", int timeoutMs = 5000) - { - if (timeoutMs > 0) { - m_ReadTimeout = timeoutMs; - } - - m_LastError = 0; - int res; - int Elapsed = Environment.TickCount; - m_client = new S7Client(); - m_client.OnDataReceived = this.OnDataReceived; - - m_client.SetConnectionParams(address, 0x0600, Encoding.ASCII.GetBytes("SIMATIC-ROOT-HMI")); - res = m_client.Connect(); - if (res != 0) - return res; - - #region Step 1: Unencrypted InitSSL Request / Response - - InitSslRequest sslReq = new InitSslRequest(ProtocolVersion.V1, 0 , 0); - res = SendS7plusFunctionObject(sslReq); - if (res != 0) - { - m_client.Disconnect(); - return res; - } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - m_client.Disconnect(); - return m_LastError; - } - InitSslResponse sslRes; - sslRes = InitSslResponse.DeserializeFromPdu(m_ReceivedPDU); - if (sslRes == null) - { - m_client.Disconnect(); - return S7Consts.errInitSslResponse; - } - - #endregion - - #region Step 2: Activate TLS. Everything from here onwards is TLS encrypted. - - res = m_client.SslActivate(); - if (res != 0) - { - m_client.Disconnect(); - return res; - } - - #endregion - - #region Step 3: CreateObjectRequest / Response (with TLS) - - var createObjReq = new CreateObjectRequest(ProtocolVersion.V1, 0, false); - createObjReq.SetNullServerSessionData(); - res = SendS7plusFunctionObject(createObjReq); - if (res != 0) - { - m_client.Disconnect(); - return res; - } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - m_client.Disconnect(); - return m_LastError; - } - - var createObjRes = CreateObjectResponse.DeserializeFromPdu(m_ReceivedPDU); - if (createObjRes == null) - { - //Console.WriteLine("S7CommPlusConnection - Connect: CreateObjectResponse with Error!"); - m_client.Disconnect(); - return S7Consts.errIsoInvalidPDU6; - } - // There are (always?) at least two IDs in the response. - // Usually the first is used for polling data, and the 2nd for jobs which use notifications, e.g. alarming, subscriptions. - m_SessionId = createObjRes.ObjectIds[0]; - m_SessionId2 = createObjRes.ObjectIds[1]; - //Console.WriteLine("S7CommPlusConnection - Connect: Using SessionId=0x" + String.Format("{0:X04}", m_SessionId)); - - // Evaluate Struct 314 - PValue sval = createObjRes.ResponseObject.GetAttribute(Ids.ServerSessionVersion); - ValueStruct serverSession = (ValueStruct)sval; - - #endregion - - #region Step 4: SetMultiVariablesRequest / Response - - var setMultiVarReq = new SetMultiVariablesRequest(ProtocolVersion.V2); - setMultiVarReq.SetSessionSetupData(m_SessionId, serverSession); - res = SendS7plusFunctionObject(setMultiVarReq); - if (res != 0) - { - m_client.Disconnect(); - return res; - } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - m_client.Disconnect(); - return m_LastError; - } - - var setMultiVarRes = SetMultiVariablesResponse.DeserializeFromPdu(m_ReceivedPDU); - if (setMultiVarRes == null) - { - //Console.WriteLine("S7CommPlusConnection - Connect: SetMultiVariablesResponse with Error!"); - m_client.Disconnect(); - return S7Consts.errIsoInvalidPDU7; - } - - #endregion - - #region Step 5: Read SystemLimits - res = m_CommRessources.ReadMax(this); - if (res != 0) - { - m_client.Disconnect(); - return res; - } - #endregion - - #region Step 6: Password - res = legitimate(serverSession, password, username); - if (res != 0) { - m_client.Disconnect(); - return res; - } - #endregion - - // If everything has been error-free up to this point, then the connection has been established successfully. - Console.WriteLine("S7CommPlusConnection - Connect: Time for connection establishment: " + (Environment.TickCount - Elapsed) + " ms."); - return 0; - } - - public void Disconnect() - { - DeleteObject(m_SessionId); - m_client.Disconnect(); - } - - /// - /// Deletes the object with the given Id. - /// - /// The object Id to delete - /// 0 on success - private int DeleteObject(uint deleteObjectId) - { - int res; - var delObjReq = new DeleteObjectRequest(ProtocolVersion.V2); - delObjReq.DeleteObjectId = deleteObjectId; - res = SendS7plusFunctionObject(delObjReq); - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - return m_LastError; - } - // If we delete our own session id, then there's no IntegrityId in the response. - // And the error code gives an error, but not a fatal one. - // If we delete another object, there should be an IntegrityId in the response, and - // the response gives no error. - if (deleteObjectId == m_SessionId) - { - var delObjRes = DeleteObjectResponse.DeserializeFromPdu(m_ReceivedPDU, false); - Trace.WriteLine("S7CommPlusConnection - DeleteSession: Deleted our own Session Id object, not checking the response."); - m_SessionId = 0; // not valid anymore - m_SessionId2 = 0; - } - else - { - var delObjRes = DeleteObjectResponse.DeserializeFromPdu(m_ReceivedPDU, true); - res = checkResponseWithIntegrity(delObjReq, delObjRes); - if (res != 0) - { - return res; - } - if (delObjRes.ReturnValue != 0) - { - Console.WriteLine("S7CommPlusConnection - DeleteSession: Executed with Error! ReturnValue=" + delObjRes.ReturnValue); - res = -1; - } - } - return res; - } - - public int ReadValues(List addresslist, out List values, out List errors) - { - // The requester must pass the internal type with the request, otherwise not all return values can be converted automatically. - // For example, strings are transmitted as UInt-Array. - values = new List(); - errors = new List(); - // Initialize error fields to error value - for (int i = 0; i < addresslist.Count; i++) - { - values.Add(null); - errors.Add(0xffffffffffffffff); - } - - // Split request into chunks, taking the MaxTags per request into account - int chunk_startIndex = 0; - int count_perChunk = 0; - do - { - int res; - var getMultiVarReq = new GetMultiVariablesRequest(ProtocolVersion.V2); - - getMultiVarReq.AddressList.Clear(); - count_perChunk = 0; - while (count_perChunk < m_CommRessources.TagsPerReadRequestMax && (chunk_startIndex + count_perChunk) < addresslist.Count) - { - getMultiVarReq.AddressList.Add(addresslist[chunk_startIndex + count_perChunk]); - count_perChunk++; - } - - res = SendS7plusFunctionObject(getMultiVarReq); - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - return m_LastError; - } - - var getMultiVarRes = GetMultiVariablesResponse.DeserializeFromPdu(m_ReceivedPDU); - res = checkResponseWithIntegrity(getMultiVarReq, getMultiVarRes); - if (res != 0) - { - return res; - } - // ReturnValue shows also an error, if only one single variable could not be read - if (getMultiVarRes.ReturnValue != 0) - { - Console.WriteLine("S7CommPlusConnection - ReadValues: Executed with Error! ReturnValue=" + getMultiVarRes.ReturnValue); - } - - // TODO: If a variable could not be read, there is no value, but there is an ErrorValue. - // The user must therefore check whether Value != null. Maybe there's a more elegant solution. - foreach (var v in getMultiVarRes.Values) - { - values[chunk_startIndex + (int)v.Key - 1] = v.Value; - // Initialize error to 0, will be overwritten below if there was an error on an item. - errors[chunk_startIndex + (int)v.Key - 1] = 0; - } - - foreach (var ev in getMultiVarRes.ErrorValues) - { - errors[chunk_startIndex + (int)ev.Key - 1] = ev.Value; - } - chunk_startIndex += count_perChunk; - - } while (chunk_startIndex < addresslist.Count); - - return m_LastError; - } - - public int WriteValues(List addresslist, List values, out List errors) - { - int res; - errors = new List(); - for (int i = 0; i < addresslist.Count; i++) - { - // Initialize to no error value, as there's no explicit value for write success. - errors.Add(0); - } - - // Split request into chunks, taking the MaxTags per request into account - int chunk_startIndex = 0; - int count_perChunk = 0; - do - { - var setMultiVarReq = new SetMultiVariablesRequest(ProtocolVersion.V2); - setMultiVarReq.AddressListVar.Clear(); - setMultiVarReq.ValueList.Clear(); - count_perChunk = 0; - while (count_perChunk < m_CommRessources.TagsPerWriteRequestMax && (chunk_startIndex + count_perChunk) < addresslist.Count) - { - setMultiVarReq.AddressListVar.Add(addresslist[chunk_startIndex + count_perChunk]); - setMultiVarReq.ValueList.Add(values[chunk_startIndex + count_perChunk]); - count_perChunk++; - } - - res = SendS7plusFunctionObject(setMultiVarReq); - if (res != 0) - { - return res; - } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - return m_LastError; - } - - var setMultiVarRes = SetMultiVariablesResponse.DeserializeFromPdu(m_ReceivedPDU); - res = checkResponseWithIntegrity(setMultiVarReq, setMultiVarRes); - if (res != 0) - { - return res; - } - // ReturnValue shows also an error, if only one single variable could not be written - if (setMultiVarRes.ReturnValue != 0) - { - Console.WriteLine("S7CommPlusConnection - WriteValues: Write with errors. ReturnValue=" + setMultiVarRes.ReturnValue); - } - - foreach (var ev in setMultiVarRes.ErrorValues) - { - errors[chunk_startIndex + (int)ev.Key - 1] = ev.Value; - } - chunk_startIndex += count_perChunk; - - } while (chunk_startIndex < addresslist.Count); - - return m_LastError; - } - - public int SetPlcOperatingState(Int32 state) - { - int res; - var setVarReq = new SetVariableRequest(ProtocolVersion.V2); - setVarReq.InObjectId = Ids.NativeObjects_theCPUexecUnit_Rid; - setVarReq.Address = Ids.CPUexecUnit_operatingStateReq; - setVarReq.Value = new ValueDInt(state); - - res = SendS7plusFunctionObject(setVarReq); - if (res != 0) - { - m_client.Disconnect(); - return res; - } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - m_client.Disconnect(); - return m_LastError; - } - - var setVarRes = SetVariableResponse.DeserializeFromPdu(m_ReceivedPDU); - if (setVarRes == null) - { - //Console.WriteLine("S7CommPlusConnection - Connect: SetVariableResponse with Error!"); - m_client.Disconnect(); - return S7Consts.errIsoInvalidPDU12; - } - - return 0; - } - - public int Browse(out List varInfoList) - { - int res; - varInfoList = new List(); - Browser vars = new Browser(); - ExploreRequest exploreReq; - ExploreResponse exploreRes; - - #region Read all objects - - var exploreData = new List(); - - exploreReq = new ExploreRequest(ProtocolVersion.V2); - exploreReq.ExploreId = Ids.NativeObjects_thePLCProgram_Rid; - exploreReq.ExploreRequestId = Ids.None; - exploreReq.ExploreChildsRecursive = 1; - exploreReq.ExploreParents = 0; - - // We want to know the following attributes - exploreReq.AddressList.Add(Ids.ObjectVariableTypeName); - exploreReq.AddressList.Add(Ids.Block_BlockNumber); - exploreReq.AddressList.Add(Ids.ASObjectES_Comment); - - res = SendS7plusFunctionObject(exploreReq); - if (res != 0) - { - return res; - } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - return m_LastError; - } - - exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); - res = checkResponseWithIntegrity(exploreReq, exploreRes); - if (res != 0) - { - return res; - } - - #endregion - - #region Evaluate all data blocks that then need to be browsed - - var obj = exploreRes.Objects.First(o => o.ClassId == Ids.PLCProgram_Class_Rid); - - foreach (var ob in obj.GetObjects()) - { - switch (ob.ClassId) - { - case Ids.DB_Class_Rid: - UInt32 relid = ob.RelationId; - UInt32 area = (relid >> 16); - UInt32 num = relid & 0xffff; - if (area == 0x8a0e) - { - var name = (ValueWString)(ob.GetAttribute(Ids.ObjectVariableTypeName)); - BrowseData data = new BrowseData(); - data.db_block_relid = relid; - data.db_name = name.GetValue(); - data.db_number = num; - exploreData.Add(data); - } - break; - } - } - - #endregion - - #region Determine the TypeInfo RID to the RelId from the first response - // By querying LID = 1 from all DBs you get the RID back with which the type information can be queried. - // This is neccessary because, for example, with instance DBs (e.g. TON), the type information must - // not be accessed via the RID of the DB, but of the RID of the TON. - var readlist = new List(); - var values = new List(); - var errors = new List(); - - foreach (var data in exploreData) - { - if (data.db_number > 0) // only process datablocks here, no marker, timer etc. - { - // Insert the variable address - var adr1 = new ItemAddress(); - adr1.AccessArea = data.db_block_relid; - adr1.AccessSubArea = Ids.DB_ValueActual; - adr1.LID.Add(1); - readlist.Add(adr1); - } - } - res = ReadValues(readlist, out values, out errors); - if (res != 0) - { - return res; - } - #endregion - - #region Pass the preliminary information for recombination to ExploreSymbols - - // Add the response information to the list - for (int i = 0; i < values.Count; i++) - { - if (errors[i] == 0) - { - ValueRID rid = (ValueRID)values[i]; - var data = exploreData[i]; - data.db_block_ti_relid = rid.GetValue(); - exploreData[i] = data; - } - else - { - // On error, set the relid to zero, will be removed from the list in the next step. - // TODO: Report this as an error? - var data = exploreData[i]; - data.db_block_ti_relid = 0; - exploreData[i] = data; - } - } - // Remove elements with db_block_ti_relid == 0. This occurs e.g. on datablocks only present in load memory. - // The informations can't be used any further (at least not for variable access). - exploreData.RemoveAll(item => item.db_block_ti_relid == 0); - - foreach (var ed in exploreData) - { - vars.AddBlockNode(eNodeType.Root, ed.db_name, ed.db_block_relid, ed.db_block_ti_relid); - } - - // Add IQMCT areas manually - vars.AddBlockNode(eNodeType.Root, "IArea", Ids.NativeObjects_theIArea_Rid, 0x90010000); - vars.AddBlockNode(eNodeType.Root, "QArea", Ids.NativeObjects_theQArea_Rid, 0x90020000); - vars.AddBlockNode(eNodeType.Root, "MArea", Ids.NativeObjects_theMArea_Rid, 0x90030000); - vars.AddBlockNode(eNodeType.Root, "S7Timers", Ids.NativeObjects_theS7Timers_Rid, 0x90050000); - vars.AddBlockNode(eNodeType.Root, "S7Counters", Ids.NativeObjects_theS7Counters_Rid, 0x90060000); - - #endregion - - #region Read the Type Info Container (as a single big PDU, must be proven to be the way to go in big programs) - exploreReq = new ExploreRequest(ProtocolVersion.V2); - // With ObjectOMSTypeInfoContainer we get all in a big PDU (with maybe hundreds of fragments) - exploreReq.ExploreId = Ids.ObjectOMSTypeInfoContainer; - exploreReq.ExploreRequestId = Ids.None; - exploreReq.ExploreChildsRecursive = 1; - exploreReq.ExploreParents = 0; - - res = SendS7plusFunctionObject(exploreReq); - if (res != 0) - { - return res; - } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - return m_LastError; - } - #endregion - - #region Process the response, and build the complete variables list - exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); - res = checkResponseWithIntegrity(exploreReq, exploreRes); - if (res != 0) - { - return res; - } - var objs = exploreRes.Objects.First(o => o.ClassId == Ids.ClassOMSTypeInfoContainer); - - vars.SetTypeInfoContainerObjects(objs.GetObjects()); - vars.BuildTree(); - vars.BuildFlatList(); - varInfoList = vars.GetVarInfoList(); - #endregion - - return 0; - } - - /// - /// Gets the first level of a tag symbol string. Removes the " used to escape special chars. - /// - /// plc tag symbol - /// The first level of the symbol string - /// Symbol syntax error - private string parseSymbolLevel(ref string symbol) - { - if (symbol.StartsWith("\"")) - { - int idx = symbol.IndexOf('"', 1); - if (idx < 0) throw new Exception("Symbol syntax error"); - string lvl = symbol.Substring(1, idx - 1); - symbol = symbol.Remove(0, idx + 1); - if (symbol.StartsWith(".")) symbol = symbol.Remove(0, 1); - return lvl; - } - else - { - int idx = symbol.IndexOf('.'); - int idx2 = symbol.IndexOf('[', 1); - if (idx2 >= 0 && (idx2 < idx || idx < 0)) idx = idx2; - if (idx >= 0) - { - string lvl = symbol.Substring(0, idx); - symbol = symbol.Remove(0, idx); - if (symbol.StartsWith(".")) symbol = symbol.Remove(0, 1); - return lvl; - } - else - { - string lvl = symbol; - symbol = ""; - return lvl; - } - } - } - - /// - /// Gets the typeinfo by given ti relid from the internal buffer. If it's not found in the buffer - /// it's fetched from the PLC and stored in the buffer. - /// - /// type info relid - /// type info - /// Could not get type info - public PObject getTypeInfoByRelId(uint ti_relid) - { - PObject pObj = typeInfoList.Find(ti => ti.RelationId == ti_relid); - if (pObj == null) - { - // Type info not found in list, request it from plc - List newPObj = new List(); - if (GetTypeInformation(ti_relid, out newPObj) != 0) throw new Exception("Could not get type info"); - typeInfoList.AddRange(newPObj); - // Try again - pObj = typeInfoList.Find(ti => ti.RelationId == ti_relid); - } - return pObj; - } - - /// - /// Calculates the access sequence for 1 dimensional arrays. - /// - /// plc tag symbol - /// Var type that holds the dim info - /// used to build access sequence - /// Symbol syntax error - private void calcAccessSeqFor1DimArray(ref string symbol, PVartypeListElement varType, VarInfo varInfo) - { - Regex re = new Regex(@"^\[(-?\d+)\]"); - Match m = re.Match(symbol); - if (!m.Success) throw new Exception("Symbol syntax error"); - parseSymbolLevel(ref symbol); // remove index from symbol string - int arrayIndex = int.Parse(m.Groups[1].Value); - - var ioit = (IOffsetInfoType_1Dim)varType.OffsetInfoType; - uint arrayElementCount = ioit.GetArrayElementCount(); - int arrayLowerBounds = ioit.GetArrayLowerBounds(); - - if (arrayIndex - arrayLowerBounds > arrayElementCount) throw new Exception("Out of bounds"); - if (arrayIndex < arrayLowerBounds) throw new Exception("Out of bounds"); - varInfo.AccessSequence += "." + String.Format("{0:X}", arrayIndex - arrayLowerBounds); - if (varType.OffsetInfoType.HasRelation()) varInfo.AccessSequence += ".1"; // additional ".1" for array of struct - } - - /// - /// Calculates the access sequence for multi-dimensional arrays. - /// - /// plc tag symbol - /// Var type that holds the dim info - /// used to build access sequence - /// Symbol syntax error - private void calcAccessSeqForMDimArray(ref string symbol, PVartypeListElement varType, VarInfo varInfo) - { - Regex re = new Regex(@"^\[( ?-?\d+ ?(, ?-?\d+ ?)+)\]"); - Match m = re.Match(symbol); - if (!m.Success) throw new Exception("Symbol syntax error"); - parseSymbolLevel(ref symbol); // remove index from symbol string - string idxs = m.Groups[1].Value.Replace(" ", ""); - - int[] indexes = Array.ConvertAll(idxs.Split(','), e => int.Parse(e)); - var ioit = (IOffsetInfoType_MDim)varType.OffsetInfoType; - uint[] MdimArrayElementCount = (uint[])ioit.GetMdimArrayElementCount().Clone(); - int[] MdimArrayLowerBounds = ioit.GetMdimArrayLowerBounds(); - - // check dim count - int dimCount = MdimArrayElementCount.Aggregate(0, (acc, act) => acc += (act > 0) ? 1 : 0); - if (dimCount != indexes.Count()) throw new Exception("Out of bounds"); - // check bounds - for (int i = 0; i < dimCount; ++i) - { - indexes[i] = (indexes[i] - MdimArrayLowerBounds[dimCount - i - 1]); - if (indexes[i] > MdimArrayElementCount[dimCount - i - 1]) throw new Exception("Out of bounds"); - if (indexes[i] < 0) throw new Exception("Out of bounds"); - } - - // calc dim size - if (varType.Softdatatype == Softdatatype.S7COMMP_SOFTDATATYPE_BBOOL) - { - MdimArrayElementCount[0] += 8 - MdimArrayElementCount[0] % 8; // for bool must be a mutiple of 8! - } - uint[] dimSize = new uint[dimCount]; - uint g = 1; - for (int i = 0; i < dimCount - 1; ++i) - { - dimSize[i] = g; - g *= MdimArrayElementCount[i]; - } - dimSize[dimCount - 1] = g; - - // calc id - int arrayIndex = 0; - for (int i = 0; i < dimCount; ++i) - { - arrayIndex += indexes[i] * (int)dimSize[dimCount - i - 1]; - } - - varInfo.AccessSequence += "." + String.Format("{0:X}", arrayIndex); - if (varType.OffsetInfoType.HasRelation()) varInfo.AccessSequence += ".1"; // additional ".1" for array of struct - } - - /// - /// Browses the symbol level by level recursively. Fetches missing type info automatically from the plc. - /// - /// type info relid - /// plc tag symbol - /// used to build access sequence - /// plc tag or null if not found - /// Symbol syntax error, Out of bounds - private PlcTag browsePlcTagBySymbol(uint ti_relid, ref string symbol, VarInfo varInfo) - { - PObject pObj = getTypeInfoByRelId(ti_relid); - if (pObj == null) throw new Exception("Could not get type info"); - string levelName = parseSymbolLevel(ref symbol); - // find level name of symbol in var list - int idx = pObj.VarnameList?.Names?.IndexOf(levelName) ?? -1; - if (idx < 0) return null; - PVartypeListElement varType = pObj.VartypeList.Elements[idx]; - varInfo.AccessSequence += "." + String.Format("{0:X}", varType.LID); - if (varType.OffsetInfoType.Is1Dim()) - { - calcAccessSeqFor1DimArray(ref symbol, varType, varInfo); - } - if (varType.OffsetInfoType.IsMDim()) - { - calcAccessSeqForMDimArray(ref symbol, varType, varInfo); - } - if (varType.OffsetInfoType.HasRelation()) - { - if (symbol.Length <= 0) - { - return null; - } - else - { - var ioit = (IOffsetInfoType_Relation)varType.OffsetInfoType; - return browsePlcTagBySymbol(ioit.GetRelationId(), ref symbol, varInfo); - } - } - else - { - return PlcTags.TagFactory(varInfo.Name, new ItemAddress(varInfo.AccessSequence), varType.Softdatatype); - } - } - - /// - /// Get the plc tag for the given plc tag symbol. - /// - /// plc tag symbol - /// plc tag, returns null if plc tag could not be found - public PlcTag getPlcTagBySymbol(string symbol) - { - VarInfo varInfo = new VarInfo(); - varInfo.Name = symbol; - // make sure we have the db list - if (dbInfoList == null) - { - if (GetListOfDatablocks(out dbInfoList) != 0) { return null; } - } - string levelName = parseSymbolLevel(ref symbol); - // find db by first level name of symbol - DatablockInfo dbInfo = dbInfoList.Find(dbi => dbi.db_name == levelName); - if (dbInfo != null) - { - varInfo.AccessSequence = String.Format("{0:X}", dbInfo.db_block_relid); - return browsePlcTagBySymbol(dbInfo.db_block_ti_relid, ref symbol, varInfo); - } - else - { - symbol = varInfo.Name; - // Merker - varInfo.AccessSequence = String.Format("{0:X}", Ids.NativeObjects_theMArea_Rid); - PlcTag tag = browsePlcTagBySymbol(0x90030000, ref symbol, varInfo); - if (tag != null) return tag; - symbol = varInfo.Name; - // Outputs - varInfo.AccessSequence = String.Format("{0:X}", Ids.NativeObjects_theQArea_Rid); - tag = browsePlcTagBySymbol(0x90020000, ref symbol, varInfo); - if (tag != null) return tag; - symbol = varInfo.Name; - // Inputs - varInfo.AccessSequence = String.Format("{0:X}", Ids.NativeObjects_theIArea_Rid); - tag = browsePlcTagBySymbol(0x90010000, ref symbol, varInfo); - if (tag != null) return tag; - // TODO: implement s5timers and counters... no one uses them anymore anyway - } - return null; - } - - public class BrowseEntry - { - public string Name; - public uint Softdatatype; - public UInt32 LID; - public UInt32 SymbolCrc; - public string AccessSequence; - }; - - public class BrowseData - { - public string db_name; // Name of the datablock - public UInt32 db_number; // Number of the datablock - public UInt32 db_block_relid; // RID of the datablock - public UInt32 db_block_ti_relid; // Type-Info RID of the datablock - public List variables = new List(); // Variables inside the datablock - }; - - public class DatablockInfo - { - public string db_name; // Name of the datablock - public UInt32 db_number; // Number of the datablock - public UInt32 db_block_relid; // RID of the datablock - public UInt32 db_block_ti_relid; // Type-Info RID of the datablock - }; - - public int GetListOfDatablocks(out List dbInfoList) - { - int res; - - dbInfoList = new List(); - - var exploreReq = new ExploreRequest(ProtocolVersion.V2); - exploreReq.ExploreId = Ids.NativeObjects_thePLCProgram_Rid; - exploreReq.ExploreRequestId = Ids.None; - exploreReq.ExploreChildsRecursive = 1; - exploreReq.ExploreParents = 0; - - // Add the attributes we need in the response - exploreReq.AddressList.Add(Ids.ObjectVariableTypeName); - - // Set filter on Id for Datablock Class RID. With this filter, we only - // get informations from datablocks, and not other blocks we don't need here. - var filter = new ValueStruct(Ids.Filter); - filter.AddStructElement(Ids.FilterOperation, new ValueDInt(8)); // 8 = InstanceIOf - filter.AddStructElement(Ids.AddressCount, new ValueUDInt(0)); - uint[] faddress = new uint[32]; // Unknown, possible dependant on FilterOperation - filter.AddStructElement(Ids.Address, new ValueUDIntArray(faddress)); - filter.AddStructElement(Ids.FilterValue, new ValueRID(Ids.DB_Class_Rid)); - - exploreReq.FilterData = filter; - - res = SendS7plusFunctionObject(exploreReq); - if (res != 0) - { - return res; - } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - return m_LastError; - } - - var exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); - res = checkResponseWithIntegrity(exploreReq, exploreRes); - if (res != 0) - { - return res; - } - - // Get the datablock information we want further informations from. - var objList = exploreRes.Objects; - - foreach (var ob in objList) - { - // May be this check can be removed, if setting the filter to the DB_Class_Rid is working 100%. - switch (ob.ClassId) - { - case Ids.DB_Class_Rid: - UInt32 relid = ob.RelationId; - UInt32 area = (relid >> 16); - UInt32 num = relid & 0xffff; - if (area == 0x8a0e) - { - var name = (ValueWString)(ob.GetAttribute(Ids.ObjectVariableTypeName)); - DatablockInfo data = new DatablockInfo(); - data.db_block_relid = relid; - data.db_name = name.GetValue(); - data.db_number = num; - dbInfoList.Add(data); - } - break; - } - } - - // Get the TypeInfo RID to RelId from the first response - - // With LID=1 we get the RID back. With this number we can explore further - // informations of this datablock. - // This is neccessary, because informations about instance DBs (e.g. TON) you - // don't get by the RID of the DB, instead of exploring the TON Type RID. - var readlist = new List(); - var values = new List(); - var errors = new List(); - - foreach (var data in dbInfoList) - { - if (data.db_number > 0) - { - // Insert the address - var adr1 = new ItemAddress(); - adr1.AccessArea = data.db_block_relid; - adr1.AccessSubArea = Ids.DB_ValueActual; - adr1.LID.Add(1); - readlist.Add(adr1); - } - } - res = ReadValues(readlist, out values, out errors); - if (res != 0) - { - return res; - } - - // Insert response data into the list - for (int i = 0; i < values.Count; i++) - { - if (errors[i] == 0) - { - var rid = (ValueRID)values[i]; - var data = dbInfoList[i]; - data.db_block_ti_relid = rid.GetValue(); - dbInfoList[i] = data; - } - else - { - // On error, set relid=0, which is then removed in the next step. - // Should we report this for the user? - var data = dbInfoList[i]; - data.db_block_ti_relid = 0; - dbInfoList[i] = data; - } - } - - // Remove elements with db_block_ti_relid == 0. - // This can occur on datablocks which are only in load memory and can't be explored. - dbInfoList.RemoveAll(item => item.db_block_ti_relid == 0); - - return 0; - } - - public int GetTypeInformation(uint exploreId, out List objList) - { - int res; - objList = new List(); - - var exploreReq = new ExploreRequest(ProtocolVersion.V2); - exploreReq.ExploreId = exploreId; - exploreReq.ExploreRequestId = Ids.None; - exploreReq.ExploreChildsRecursive = 1; - exploreReq.ExploreParents = 0; - - res = SendS7plusFunctionObject(exploreReq); - if (res != 0) - { - return res; - } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - return m_LastError; - } - - var exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); - res = checkResponseWithIntegrity(exploreReq, exploreRes); - if (res != 0) - { - return res; - } - objList = exploreRes.Objects; - - return 0; - } - - /// - /// Requests the tag and block comments from the Plc, returned as XML strings. - /// xml_linecomment: - /// The returned XML format differs between between request of I/Q/M/C/T areas and datablocks: - /// I/Q/M/C/T: .... - /// Datablock: .... - /// As "ID" the number for the variable identification is used. - /// - /// xml_dbcomment: - /// The xml-value description generated from our own value xml-serialization for WStringSparseArray. The value key is the language id. - /// Example: - /// DB Kommentar in german de-DEDB comment in english en-US - /// - /// The relation ID for the area you want the comments for, e.g. 0x8a0e0000+db_number, or 0x52 for M-area - /// - /// - /// 0 if no error - public int GetCommentsXml(uint relid, out string xml_linecomment, out string xml_dbcomment) - { - int res; - // With requesting DataInterface_InterfaceDescription, whe would be able to get all informations like the access ids and - // datatype informations, that we get from the other browsing method. Needs to be tested which one is more efficient on network traffic or plc load. - // If we keep use browsing for the comments, at least we would be able to read all information in one request. - xml_linecomment = String.Empty; - xml_dbcomment = String.Empty; - - var exploreReq = new ExploreRequest(ProtocolVersion.V2); - exploreReq.ExploreId = relid; - exploreReq.ExploreRequestId = Ids.None; - exploreReq.ExploreChildsRecursive = 1; - exploreReq.ExploreParents = 0; - - // We want to know the following attributes - exploreReq.AddressList.Add(Ids.ASObjectES_Comment); - exploreReq.AddressList.Add(Ids.DataInterface_LineComments); - - res = SendS7plusFunctionObject(exploreReq); - if (res != 0) - { - return res; - } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - return m_LastError; - } - - var exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); - res = checkResponseWithIntegrity(exploreReq, exploreRes); - if (res != 0) - { - return res; - } - - foreach(var obj in exploreRes.Objects) - { - foreach(var att in obj.Attributes) - { - switch (att.Key) - { - case Ids.ASObjectES_Comment: - var att_comment = (ValueWStringSparseArray)att.Value; - xml_dbcomment = att_comment.ToString(); - break; - case Ids.DataInterface_LineComments: - var att_linecomment = (ValueBlobSparseArray)att.Value; - BlobDecompressor bd = new BlobDecompressor(); - var blob_sp = att_linecomment.GetValue(); - // In DBs we get the data with Sparsearray key = 1, in M-Area with key = 2. - // For now, just take the first, don't know where the key ids are for. - foreach (var key in blob_sp.Keys) - { - xml_linecomment = bd.decompress(blob_sp[key].value, 4); // Offset of 4, as we have a header for the zlib dictionary version - break; - } - break; - } - } - } - return 0; - } - } - #endregion -} +#region License +/****************************************************************************** + * S7CommPlusDriver + * + * Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de + * + * This file is part of S7CommPlusDriver. + * + * S7CommPlusDriver is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + /****************************************************************************/ +#endregion + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.IO; +using System.Linq; +using System.Diagnostics; +using S7CommPlusDriver.ClientApi; +using System.Text.RegularExpressions; +using System.Security.Cryptography; + +namespace S7CommPlusDriver +{ + public partial class S7CommPlusConnection + { + #region Private Members + private S7Client m_client; + private MemoryStream m_ReceivedPDU; + private MemoryStream m_ReceivedTempPDU; + private Queue m_ReceivedPDUs = new Queue(); + private Mutex m_Mutex = new Mutex(); + + private bool m_ReceivedNeedMoreDataForCompletePDU; + private bool m_NewS7CommPlusReceived; + private UInt32 m_SessionId; + private UInt32 m_SessionId2; + public UInt32 SessionId2 + { + get { return m_SessionId2; } + private set { m_SessionId2 = value; } + } + + private int m_ReadTimeout = 5000; + private UInt16 m_SequenceNumber = 0; + private UInt32 m_IntegrityId = 0; + private UInt32 m_IntegrityId_Set = 0; + private CommRessources m_CommRessources = new CommRessources(); + + private List dbInfoList; + private List typeInfoList = new List(); + #endregion + + #region Public Members + public int m_LastError = 0; + + #endregion + + #region Private Methods + + private UInt16 GetNextSequenceNumber() + { + if (m_SequenceNumber == UInt16.MaxValue) + { + m_SequenceNumber = 1; + } + else + { + m_SequenceNumber++; + } + return m_SequenceNumber; + } + + // We must count the IntegrityId for different functions of the protocol. + // As a first guess functions for setting variables need separate counters. + // Use the functioncode to differ between the which sequence/integrity counter values. + private UInt32 GetNextIntegrityId(ushort functioncode) + { + UInt32 ret; + switch (functioncode) + { + case Functioncode.SetMultiVariables: + case Functioncode.SetVariable: + case Functioncode.SetVarSubStreamed: + case Functioncode.DeleteObject: + case Functioncode.CreateObject: + if (m_IntegrityId_Set == UInt32.MaxValue) + { + m_IntegrityId_Set = 0; + } + else + { + m_IntegrityId_Set++; + } + ret = m_IntegrityId_Set; + break; + default: + if (m_IntegrityId == UInt32.MaxValue) + { + m_IntegrityId = 0; + } + else + { + m_IntegrityId++; + } + ret = m_IntegrityId; + break; + } + return ret; + } + + private void WaitForNewS7plusReceived(int Timeout) + { + bool Expired = false; + Stopwatch Elapsed = Stopwatch.StartNew(); + bool done = false; + + m_Mutex.WaitOne(); + if (m_ReceivedPDUs.Count > 0) + { + m_ReceivedPDU = m_ReceivedPDUs.Dequeue(); + done = true; + } + m_Mutex.ReleaseMutex(); + + while (!done && !Expired) + { + Thread.Sleep(2); + Expired = Elapsed.ElapsedMilliseconds > Timeout; + m_Mutex.WaitOne(); + if (m_ReceivedPDUs.Count > 0) + { + m_ReceivedPDU = m_ReceivedPDUs.Dequeue(); + done = true; + } + m_Mutex.ReleaseMutex(); + } + + if (Expired) + { + Console.WriteLine("S7CommPlusConnection - WaitForNewS7plusReceived: ERROR: Timeout!"); + m_LastError = S7Consts.errTCPDataReceive; + } + } + + private int SendS7plusFunctionObject(IS7pRequest funcObj) + { + // If we don't have a SessionId, this must be the first CreateObjectRequest, where we use the Id for NullServerSession + if (m_SessionId == 0) + { + funcObj.SessionId = Ids.ObjectNullServerSession; + } + else + { + funcObj.SessionId = m_SessionId; + } + + // Insert SequenceNumber and IntegrityId, if neccessary for object type and state of communication + funcObj.SequenceNumber = GetNextSequenceNumber(); + if (funcObj.WithIntegrityId) + { + funcObj.IntegrityId = GetNextIntegrityId(funcObj.FunctionCode); + } + + MemoryStream stream = new MemoryStream(); + funcObj.Serialize(stream); + return SendS7plusPDUdata(stream.ToArray(), (int)stream.Length, funcObj.ProtocolVersion); + } + + private int SendS7plusPDUdata(byte[] sendPduData, int bytesToSend, byte protoVersion) + { + m_LastError = 0; + + int curSize; + int sourcePos = 0; + int sendLen; + int NegotiatedIsoPduSize = 1024;// TODO: Respect the negotiated TPDU size + + // 4 Byte TPKT Header + // 3 Byte ISO-Header + // 5 Byte TLS Header + 17 Bytes addition from TLS + // 4 Byte S7CommPlus Header + // 4 Byte S7CommPlus Trailer (must fit into last PDU) + int MaxSize = NegotiatedIsoPduSize - 4 - 3 - 5 - 17 - 4 - 4; + byte[] packet = new byte[MaxSize + 4]; //max packet size is always MaxSize + PDU Header + + while (bytesToSend > 0) + { + if (bytesToSend > MaxSize) + { + curSize = MaxSize; + bytesToSend -= MaxSize; + } + else + { + curSize = bytesToSend; + bytesToSend -= curSize; + } + // Header + packet[0] = 0x72; + packet[1] = protoVersion; + packet[2] = (byte)(curSize >> 8); + packet[3] = (byte)(curSize & 0x00FF); + // Data part + Array.Copy(sendPduData, sourcePos, packet, 4, curSize); + sourcePos += curSize; + sendLen = 4 + curSize; + + // Trailer only in last packet + if (bytesToSend == 0) + { + Array.Resize(ref packet, sendLen + 4); //resize only the last package to sendLen + TrailerLen + packet[sendLen] = 0x72; + sendLen++; + packet[sendLen] = protoVersion; + sendLen++; + packet[sendLen] = 0; + sendLen++; + packet[sendLen] = 0; + sendLen++; + } + m_client.Send(packet); + } + return m_LastError; + } + + private void OnDataReceived(byte[] PDU, int len) + { + // In this method, we've got always a complete TPDU (from protocol layer above) without fragmentation + // At this point, we can detect if we receive a fragmented S7CommPlus PDU. + // If not fragmented, then TPKT.Length - 15 is equal of the length in S7CommPlus.Header. + // 15 bytes because: 4 Bytes TPKT.Header.len + 3 Bytes ISO.Header.Len + 4 Bytes S7CommPlus.Header.len + 4 Bytes S7CommPlus.trailer.Len. + // Since the pure userdata of the TPDU comes in here, that is only minus 4 bytes header + 4 bytes trailer. + // + // Special handling for SystemEvents with ProtocolVersion = 0xfe: + // Here's only a header. + // Because of this, the first byte for the ProtocolVersion must be written in then stream at first. + // The datalength must not be written into the stream, because it's not valid on fragmented PDUs + // for the complete length, only for the single fragment. + + // This method is called from a different thread. + // If we use subscriptions or alarming, we may get new data before the last PDU was processed completely. + // First step we push the complete PDU to a queue. + // TODO: m_LastError handling would also not work as expected. This needs some more redesign. + + if (!m_ReceivedNeedMoreDataForCompletePDU) + { + m_ReceivedTempPDU = new MemoryStream(); + } + // S7comm-plus + byte protoVersion; + int pos = 0; + int s7HeaderDataLen = 0; + // Check header + if (PDU[pos] != 0x72) + { + m_ReceivedNeedMoreDataForCompletePDU = false; + m_LastError = S7Consts.errIsoInvalidPDU1; + return; + } + pos++; + protoVersion = PDU[pos]; + if (protoVersion != ProtocolVersion.V1 && protoVersion != ProtocolVersion.V2 && protoVersion != ProtocolVersion.V3 && protoVersion != ProtocolVersion.SystemEvent) + { + m_ReceivedNeedMoreDataForCompletePDU = false; + m_LastError = S7Consts.errIsoInvalidPDU2; + return; + } + // For the first fragment, write the ProtocolVersion into the stream in advance + if (!m_ReceivedNeedMoreDataForCompletePDU) + { + m_ReceivedTempPDU.Write(PDU, pos, 1); + } + pos++; + + // Read the length of the data-part from header + s7HeaderDataLen = GetWordAt(PDU, pos); + pos += 2; + if (s7HeaderDataLen > 0) + { + // Special handling for SystemEvent 0xfe PDUs: + // This only confirms a few data, but also reports major protocol errors (e.g. incorrect sequence numbers). + // The confirms can be discarded (for now), but the errors are relevant, because a connection termination is neccessary. + // As we don't have a trailer on this types, it's not possible that they are transmitted as fragments. + if (protoVersion == ProtocolVersion.SystemEvent) + { + Console.WriteLine("S7CommPlusConnection - OnDataReceived: ProtocolVersion 0xfe SystemEvent received"); + m_ReceivedTempPDU.Write(PDU, pos, s7HeaderDataLen); + pos += s7HeaderDataLen; + // Create SystemEventObject + m_ReceivedNeedMoreDataForCompletePDU = false; + m_ReceivedTempPDU.Position = 0; + m_NewS7CommPlusReceived = false; + + var sysevt = SystemEvent.DeserializeFromPdu(m_ReceivedTempPDU); + if (sysevt.IsFatalError()) + { + Console.WriteLine("S7CommPlusConnection - OnDataReceived: SystemEvent has fatal error"); + // Termination neccessary + m_LastError = S7Consts.errIsoInvalidPDU3; + } + else + { + Console.WriteLine("S7CommPlusConnection - OnDataReceived: SystemEvent with non fatal error, do nothing"); + } + } + else + { + // Copy data part to destination stream + m_ReceivedTempPDU.Write(PDU, pos, s7HeaderDataLen); + pos += s7HeaderDataLen; + // If this is a fragmented PDU, then at this point no trailer + if ((len - 4 - 4) == s7HeaderDataLen) + { + m_ReceivedNeedMoreDataForCompletePDU = false; + m_ReceivedTempPDU.Position = 0; // Set position back to zero, ready for readout + m_NewS7CommPlusReceived = true; + } + else + { + m_ReceivedNeedMoreDataForCompletePDU = true; + } + } + } + + // If a complete (usable) PDU is received, add to the queue (threadsafe) for readout + if (m_NewS7CommPlusReceived) + { + // Push complete PDU to the queue + m_Mutex.WaitOne(); + m_ReceivedPDUs.Enqueue(m_ReceivedTempPDU); + m_Mutex.ReleaseMutex(); + m_NewS7CommPlusReceived = false; + } + } + + private UInt16 GetWordAt(byte[] Buffer, int Pos) + { + return (UInt16)((Buffer[Pos] << 8) | Buffer[Pos + 1]); + } + + private void SetWordAt(byte[] Buffer, int Pos, UInt16 Value) + { + Buffer[Pos] = (byte)(Value >> 8); + Buffer[Pos + 1] = (byte)(Value & 0x00FF); + } + + private void printBuf(byte[] b) + { + for (int i = 0; i < b.Length; i++) + { + Console.Write("0x" + String.Format("{0:X02} ", b[i])); + } + Console.Write(Environment.NewLine); + } + + private int checkResponseWithIntegrity(IS7pRequest request, IS7pResponse response) + { + if (response == null) + { + //Console.WriteLine("checkResponseWithIntegrity: ERROR! response == null"); + return S7Consts.errIsoInvalidPDU4; + } + if (request.SequenceNumber != response.SequenceNumber) + { + //Console.WriteLine(String.Format("checkResponseWithIntegrity: ERROR! SeqenceNumber of Response ({0}) doesn't match Request ({1})", response.SequenceNumber, request.SequenceNumber)); + return S7Consts.errIsoInvalidPDU5; + } + // Overflow is possible and allowed + UInt32 reqIntegCheck = (UInt32)request.SequenceNumber + request.IntegrityId; + if (response.IntegrityId != reqIntegCheck) + { + Console.WriteLine(String.Format("checkResponseWithIntegrity: ERROR! IntegrityId of the Response ({0}) doesn't match Request ({1})", response.IntegrityId, reqIntegCheck)); + // Don't return this as error so far + } + return 0; + } + #endregion + + #region Public Methods + /// + /// Establishes a connection to the PLC. + /// + /// PLC IP address + /// PLC password (if set) + /// read timeout in milliseconds (default: 5000 ms) + /// + public int Connect(string address, string password = "", string username = "", int timeoutMs = 5000) + { + if (timeoutMs > 0) + { + m_ReadTimeout = timeoutMs; + } + + m_LastError = 0; + int res; + Stopwatch Elapsed = Stopwatch.StartNew(); + m_client = new S7Client(); + m_client.OnDataReceived = this.OnDataReceived; + + m_client.SetConnectionParams(address, 0x0600, Encoding.ASCII.GetBytes("SIMATIC-ROOT-HMI")); + try + { + m_client.Connect(); + } + catch + { + m_client.Disconnect(); + throw; + } + + #region Step 1: Unencrypted InitSSL Request / Response + + InitSslRequest sslReq = new InitSslRequest(ProtocolVersion.V1, 0, 0); + res = SendS7plusFunctionObject(sslReq); + if (res != 0) + { + m_client.Disconnect(); + return res; + } + m_LastError = 0; + WaitForNewS7plusReceived(m_ReadTimeout); + if (m_LastError != 0) + { + m_client.Disconnect(); + return m_LastError; + } + InitSslResponse sslRes; + sslRes = InitSslResponse.DeserializeFromPdu(m_ReceivedPDU); + if (sslRes == null) + { + m_client.Disconnect(); + return S7Consts.errInitSslResponse; + } + + #endregion + + #region Step 2: Activate TLS. Everything from here onwards is TLS encrypted. + + res = m_client.SslActivate(); + if (res != 0) + { + m_client.Disconnect(); + return res; + } + + #endregion + + #region Step 3: CreateObjectRequest / Response (with TLS) + + var createObjReq = new CreateObjectRequest(ProtocolVersion.V1, 0, false); + createObjReq.SetNullServerSessionData(); + res = SendS7plusFunctionObject(createObjReq); + if (res != 0) + { + m_client.Disconnect(); + return res; + } + m_LastError = 0; + WaitForNewS7plusReceived(m_ReadTimeout); + if (m_LastError != 0) + { + m_client.Disconnect(); + return m_LastError; + } + + var createObjRes = CreateObjectResponse.DeserializeFromPdu(m_ReceivedPDU); + if (createObjRes == null) + { + //Console.WriteLine("S7CommPlusConnection - Connect: CreateObjectResponse with Error!"); + m_client.Disconnect(); + return S7Consts.errIsoInvalidPDU6; + } + // There are (always?) at least two IDs in the response. + // Usually the first is used for polling data, and the 2nd for jobs which use notifications, e.g. alarming, subscriptions. + m_SessionId = createObjRes.ObjectIds[0]; + m_SessionId2 = createObjRes.ObjectIds[1]; + //Console.WriteLine("S7CommPlusConnection - Connect: Using SessionId=0x" + String.Format("{0:X04}", m_SessionId)); + + // Evaluate Struct 314 + PValue sval = createObjRes.ResponseObject.GetAttribute(Ids.ServerSessionVersion); + ValueStruct serverSession = (ValueStruct)sval; + + #endregion + + #region Step 4: SetMultiVariablesRequest / Response + + var setMultiVarReq = new SetMultiVariablesRequest(ProtocolVersion.V2); + setMultiVarReq.SetSessionSetupData(m_SessionId, serverSession); + res = SendS7plusFunctionObject(setMultiVarReq); + if (res != 0) + { + m_client.Disconnect(); + return res; + } + m_LastError = 0; + WaitForNewS7plusReceived(m_ReadTimeout); + if (m_LastError != 0) + { + m_client.Disconnect(); + return m_LastError; + } + + var setMultiVarRes = SetMultiVariablesResponse.DeserializeFromPdu(m_ReceivedPDU); + if (setMultiVarRes == null) + { + //Console.WriteLine("S7CommPlusConnection - Connect: SetMultiVariablesResponse with Error!"); + m_client.Disconnect(); + return S7Consts.errIsoInvalidPDU7; + } + + #endregion + + #region Step 5: Read SystemLimits + res = m_CommRessources.ReadMax(this); + if (res != 0) + { + m_client.Disconnect(); + return res; + } + #endregion + + #region Step 6: Password + res = legitimate(serverSession, password, username); + if (res != 0) + { + m_client.Disconnect(); + return res; + } + #endregion + + // If everything has been error-free up to this point, then the connection has been established successfully. + Console.WriteLine("S7CommPlusConnection - Connect: Time for connection establishment: " + Elapsed.ElapsedMilliseconds + " ms."); + return 0; + } + + public void Disconnect() + { + DeleteObject(m_SessionId); + m_client.Disconnect(); + } + + /// + /// Deletes the object with the given Id. + /// + /// The object Id to delete + /// 0 on success + private int DeleteObject(uint deleteObjectId) + { + int res; + var delObjReq = new DeleteObjectRequest(ProtocolVersion.V2); + delObjReq.DeleteObjectId = deleteObjectId; + res = SendS7plusFunctionObject(delObjReq); + m_LastError = 0; + WaitForNewS7plusReceived(m_ReadTimeout); + if (m_LastError != 0) + { + return m_LastError; + } + // If we delete our own session id, then there's no IntegrityId in the response. + // And the error code gives an error, but not a fatal one. + // If we delete another object, there should be an IntegrityId in the response, and + // the response gives no error. + if (deleteObjectId == m_SessionId) + { + var delObjRes = DeleteObjectResponse.DeserializeFromPdu(m_ReceivedPDU, false); + Trace.WriteLine("S7CommPlusConnection - DeleteSession: Deleted our own Session Id object, not checking the response."); + m_SessionId = 0; // not valid anymore + m_SessionId2 = 0; + } + else + { + var delObjRes = DeleteObjectResponse.DeserializeFromPdu(m_ReceivedPDU, true); + res = checkResponseWithIntegrity(delObjReq, delObjRes); + if (res != 0) + { + return res; + } + if (delObjRes.ReturnValue != 0) + { + Console.WriteLine("S7CommPlusConnection - DeleteSession: Executed with Error! ReturnValue=" + delObjRes.ReturnValue); + res = -1; + } + } + return res; + } + + public int ReadValues(List addresslist, out List values, out List errors) + { + // The requester must pass the internal type with the request, otherwise not all return values can be converted automatically. + // For example, strings are transmitted as UInt-Array. + values = new List(); + errors = new List(); + // Initialize error fields to error value + for (int i = 0; i < addresslist.Count; i++) + { + values.Add(null); + errors.Add(0xffffffffffffffff); + } + + // Split request into chunks, taking the MaxTags per request into account + int chunk_startIndex = 0; + int count_perChunk = 0; + do + { + int res; + var getMultiVarReq = new GetMultiVariablesRequest(ProtocolVersion.V2); + + getMultiVarReq.AddressList.Clear(); + count_perChunk = 0; + while (count_perChunk < m_CommRessources.TagsPerReadRequestMax && (chunk_startIndex + count_perChunk) < addresslist.Count) + { + getMultiVarReq.AddressList.Add(addresslist[chunk_startIndex + count_perChunk]); + count_perChunk++; + } + + res = SendS7plusFunctionObject(getMultiVarReq); + m_LastError = 0; + WaitForNewS7plusReceived(m_ReadTimeout); + if (m_LastError != 0) + { + return m_LastError; + } + + var getMultiVarRes = GetMultiVariablesResponse.DeserializeFromPdu(m_ReceivedPDU); + res = checkResponseWithIntegrity(getMultiVarReq, getMultiVarRes); + if (res != 0) + { + return res; + } + // ReturnValue shows also an error, if only one single variable could not be read + if (getMultiVarRes.ReturnValue != 0) + { + Console.WriteLine("S7CommPlusConnection - ReadValues: Executed with Error! ReturnValue=" + getMultiVarRes.ReturnValue); + } + + // TODO: If a variable could not be read, there is no value, but there is an ErrorValue. + // The user must therefore check whether Value != null. Maybe there's a more elegant solution. + foreach (var v in getMultiVarRes.Values) + { + values[chunk_startIndex + (int)v.Key - 1] = v.Value; + // Initialize error to 0, will be overwritten below if there was an error on an item. + errors[chunk_startIndex + (int)v.Key - 1] = 0; + } + + foreach (var ev in getMultiVarRes.ErrorValues) + { + errors[chunk_startIndex + (int)ev.Key - 1] = ev.Value; + } + chunk_startIndex += count_perChunk; + + } while (chunk_startIndex < addresslist.Count); + + return m_LastError; + } + + public int WriteValues(List addresslist, List values, out List errors) + { + int res; + errors = new List(); + for (int i = 0; i < addresslist.Count; i++) + { + // Initialize to no error value, as there's no explicit value for write success. + errors.Add(0); + } + + // Split request into chunks, taking the MaxTags per request into account + int chunk_startIndex = 0; + int count_perChunk = 0; + do + { + var setMultiVarReq = new SetMultiVariablesRequest(ProtocolVersion.V2); + setMultiVarReq.AddressListVar.Clear(); + setMultiVarReq.ValueList.Clear(); + count_perChunk = 0; + while (count_perChunk < m_CommRessources.TagsPerWriteRequestMax && (chunk_startIndex + count_perChunk) < addresslist.Count) + { + setMultiVarReq.AddressListVar.Add(addresslist[chunk_startIndex + count_perChunk]); + setMultiVarReq.ValueList.Add(values[chunk_startIndex + count_perChunk]); + count_perChunk++; + } + + res = SendS7plusFunctionObject(setMultiVarReq); + if (res != 0) + { + return res; + } + m_LastError = 0; + WaitForNewS7plusReceived(m_ReadTimeout); + if (m_LastError != 0) + { + return m_LastError; + } + + var setMultiVarRes = SetMultiVariablesResponse.DeserializeFromPdu(m_ReceivedPDU); + res = checkResponseWithIntegrity(setMultiVarReq, setMultiVarRes); + if (res != 0) + { + return res; + } + // ReturnValue shows also an error, if only one single variable could not be written + if (setMultiVarRes.ReturnValue != 0) + { + Console.WriteLine("S7CommPlusConnection - WriteValues: Write with errors. ReturnValue=" + setMultiVarRes.ReturnValue); + } + + foreach (var ev in setMultiVarRes.ErrorValues) + { + errors[chunk_startIndex + (int)ev.Key - 1] = ev.Value; + } + chunk_startIndex += count_perChunk; + + } while (chunk_startIndex < addresslist.Count); + + return m_LastError; + } + + public int SetPlcOperatingState(Int32 state) + { + int res; + var setVarReq = new SetVariableRequest(ProtocolVersion.V2); + setVarReq.InObjectId = Ids.NativeObjects_theCPUexecUnit_Rid; + setVarReq.Address = Ids.CPUexecUnit_operatingStateReq; + setVarReq.Value = new ValueDInt(state); + + res = SendS7plusFunctionObject(setVarReq); + if (res != 0) + { + m_client.Disconnect(); + return res; + } + m_LastError = 0; + WaitForNewS7plusReceived(m_ReadTimeout); + if (m_LastError != 0) + { + m_client.Disconnect(); + return m_LastError; + } + + var setVarRes = SetVariableResponse.DeserializeFromPdu(m_ReceivedPDU); + if (setVarRes == null) + { + //Console.WriteLine("S7CommPlusConnection - Connect: SetVariableResponse with Error!"); + m_client.Disconnect(); + return S7Consts.errIsoInvalidPDU12; + } + + return 0; + } + + public int Browse(out List varInfoList) + { + int res; + varInfoList = new List(); + Browser vars = new Browser(); + ExploreRequest exploreReq; + ExploreResponse exploreRes; + + #region Read all objects + + var exploreData = new List(); + + exploreReq = new ExploreRequest(ProtocolVersion.V2); + exploreReq.ExploreId = Ids.NativeObjects_thePLCProgram_Rid; + exploreReq.ExploreRequestId = Ids.None; + exploreReq.ExploreChildsRecursive = 1; + exploreReq.ExploreParents = 0; + + // We want to know the following attributes + exploreReq.AddressList.Add(Ids.ObjectVariableTypeName); + exploreReq.AddressList.Add(Ids.Block_BlockNumber); + exploreReq.AddressList.Add(Ids.ASObjectES_Comment); + + res = SendS7plusFunctionObject(exploreReq); + if (res != 0) + { + return res; + } + m_LastError = 0; + WaitForNewS7plusReceived(m_ReadTimeout); + if (m_LastError != 0) + { + return m_LastError; + } + + exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); + res = checkResponseWithIntegrity(exploreReq, exploreRes); + if (res != 0) + { + return res; + } + + #endregion + + #region Evaluate all data blocks that then need to be browsed + + var obj = exploreRes.Objects.First(o => o.ClassId == Ids.PLCProgram_Class_Rid); + + foreach (var ob in obj.GetObjects()) + { + switch (ob.ClassId) + { + case Ids.DB_Class_Rid: + UInt32 relid = ob.RelationId; + UInt32 area = (relid >> 16); + UInt32 num = relid & 0xffff; + if (area == 0x8a0e) + { + var name = (ValueWString)(ob.GetAttribute(Ids.ObjectVariableTypeName)); + BrowseData data = new BrowseData(); + data.db_block_relid = relid; + data.db_name = name.GetValue(); + data.db_number = num; + exploreData.Add(data); + } + break; + } + } + + #endregion + + #region Determine the TypeInfo RID to the RelId from the first response + // By querying LID = 1 from all DBs you get the RID back with which the type information can be queried. + // This is neccessary because, for example, with instance DBs (e.g. TON), the type information must + // not be accessed via the RID of the DB, but of the RID of the TON. + var readlist = new List(); + var values = new List(); + var errors = new List(); + + foreach (var data in exploreData) + { + if (data.db_number > 0) // only process datablocks here, no marker, timer etc. + { + // Insert the variable address + var adr1 = new ItemAddress(); + adr1.AccessArea = data.db_block_relid; + adr1.AccessSubArea = Ids.DB_ValueActual; + adr1.LID.Add(1); + readlist.Add(adr1); + } + } + res = ReadValues(readlist, out values, out errors); + if (res != 0) + { + return res; + } + #endregion + + #region Pass the preliminary information for recombination to ExploreSymbols + + // Add the response information to the list + for (int i = 0; i < values.Count; i++) + { + if (errors[i] == 0) + { + ValueRID rid = (ValueRID)values[i]; + var data = exploreData[i]; + data.db_block_ti_relid = rid.GetValue(); + exploreData[i] = data; + } + else + { + // On error, set the relid to zero, will be removed from the list in the next step. + // TODO: Report this as an error? + var data = exploreData[i]; + data.db_block_ti_relid = 0; + exploreData[i] = data; + } + } + // Remove elements with db_block_ti_relid == 0. This occurs e.g. on datablocks only present in load memory. + // The informations can't be used any further (at least not for variable access). + exploreData.RemoveAll(item => item.db_block_ti_relid == 0); + + foreach (var ed in exploreData) + { + vars.AddBlockNode(eNodeType.Root, ed.db_name, ed.db_block_relid, ed.db_block_ti_relid); + } + + // Add IQMCT areas manually + vars.AddBlockNode(eNodeType.Root, "IArea", Ids.NativeObjects_theIArea_Rid, 0x90010000); + vars.AddBlockNode(eNodeType.Root, "QArea", Ids.NativeObjects_theQArea_Rid, 0x90020000); + vars.AddBlockNode(eNodeType.Root, "MArea", Ids.NativeObjects_theMArea_Rid, 0x90030000); + vars.AddBlockNode(eNodeType.Root, "S7Timers", Ids.NativeObjects_theS7Timers_Rid, 0x90050000); + vars.AddBlockNode(eNodeType.Root, "S7Counters", Ids.NativeObjects_theS7Counters_Rid, 0x90060000); + + #endregion + + #region Read the Type Info Container (as a single big PDU, must be proven to be the way to go in big programs) + exploreReq = new ExploreRequest(ProtocolVersion.V2); + // With ObjectOMSTypeInfoContainer we get all in a big PDU (with maybe hundreds of fragments) + exploreReq.ExploreId = Ids.ObjectOMSTypeInfoContainer; + exploreReq.ExploreRequestId = Ids.None; + exploreReq.ExploreChildsRecursive = 1; + exploreReq.ExploreParents = 0; + + res = SendS7plusFunctionObject(exploreReq); + if (res != 0) + { + return res; + } + m_LastError = 0; + WaitForNewS7plusReceived(m_ReadTimeout); + if (m_LastError != 0) + { + return m_LastError; + } + #endregion + + #region Process the response, and build the complete variables list + exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); + res = checkResponseWithIntegrity(exploreReq, exploreRes); + if (res != 0) + { + return res; + } + var objs = exploreRes.Objects.First(o => o.ClassId == Ids.ClassOMSTypeInfoContainer); + + vars.SetTypeInfoContainerObjects(objs.GetObjects()); + vars.BuildTree(); + vars.BuildFlatList(); + varInfoList = vars.GetVarInfoList(); + #endregion + + return 0; + } + + /// + /// Gets the first level of a tag symbol string. Removes the " used to escape special chars. + /// + /// plc tag symbol + /// The first level of the symbol string + /// Symbol syntax error + private string parseSymbolLevel(ref string symbol) + { + if (symbol.StartsWith("\"")) + { + int idx = symbol.IndexOf('"', 1); + if (idx < 0) throw new Exception("Symbol syntax error"); + string lvl = symbol.Substring(1, idx - 1); + symbol = symbol.Remove(0, idx + 1); + if (symbol.StartsWith(".")) symbol = symbol.Remove(0, 1); + return lvl; + } + else + { + int idx = symbol.IndexOf('.'); + int idx2 = symbol.IndexOf('[', 1); + if (idx2 >= 0 && (idx2 < idx || idx < 0)) idx = idx2; + if (idx >= 0) + { + string lvl = symbol.Substring(0, idx); + symbol = symbol.Remove(0, idx); + if (symbol.StartsWith(".")) symbol = symbol.Remove(0, 1); + return lvl; + } + else + { + string lvl = symbol; + symbol = ""; + return lvl; + } + } + } + + /// + /// Gets the typeinfo by given ti relid from the internal buffer. If it's not found in the buffer + /// it's fetched from the PLC and stored in the buffer. + /// + /// type info relid + /// type info + /// Could not get type info + public PObject getTypeInfoByRelId(uint ti_relid) + { + PObject pObj = typeInfoList.Find(ti => ti.RelationId == ti_relid); + if (pObj == null) + { + // Type info not found in list, request it from plc + List newPObj = new List(); + if (GetTypeInformation(ti_relid, out newPObj) != 0) throw new Exception("Could not get type info"); + typeInfoList.AddRange(newPObj); + // Try again + pObj = typeInfoList.Find(ti => ti.RelationId == ti_relid); + } + return pObj; + } + + /// + /// Calculates the access sequence for 1 dimensional arrays. + /// + /// plc tag symbol + /// Var type that holds the dim info + /// used to build access sequence + /// Symbol syntax error + private void calcAccessSeqFor1DimArray(ref string symbol, PVartypeListElement varType, VarInfo varInfo) + { + Regex re = new Regex(@"^\[(-?\d+)\]"); + Match m = re.Match(symbol); + if (!m.Success) throw new Exception("Symbol syntax error"); + parseSymbolLevel(ref symbol); // remove index from symbol string + int arrayIndex = int.Parse(m.Groups[1].Value); + + var ioit = (IOffsetInfoType_1Dim)varType.OffsetInfoType; + uint arrayElementCount = ioit.GetArrayElementCount(); + int arrayLowerBounds = ioit.GetArrayLowerBounds(); + + if (arrayIndex - arrayLowerBounds > arrayElementCount) throw new Exception("Out of bounds"); + if (arrayIndex < arrayLowerBounds) throw new Exception("Out of bounds"); + varInfo.AccessSequence += "." + String.Format("{0:X}", arrayIndex - arrayLowerBounds); + if (varType.OffsetInfoType.HasRelation()) varInfo.AccessSequence += ".1"; // additional ".1" for array of struct + } + + /// + /// Calculates the access sequence for multi-dimensional arrays. + /// + /// plc tag symbol + /// Var type that holds the dim info + /// used to build access sequence + /// Symbol syntax error + private void calcAccessSeqForMDimArray(ref string symbol, PVartypeListElement varType, VarInfo varInfo) + { + Regex re = new Regex(@"^\[( ?-?\d+ ?(, ?-?\d+ ?)+)\]"); + Match m = re.Match(symbol); + if (!m.Success) throw new Exception("Symbol syntax error"); + parseSymbolLevel(ref symbol); // remove index from symbol string + string idxs = m.Groups[1].Value.Replace(" ", ""); + + int[] indexes = Array.ConvertAll(idxs.Split(','), e => int.Parse(e)); + var ioit = (IOffsetInfoType_MDim)varType.OffsetInfoType; + uint[] MdimArrayElementCount = (uint[])ioit.GetMdimArrayElementCount().Clone(); + int[] MdimArrayLowerBounds = ioit.GetMdimArrayLowerBounds(); + + // check dim count + int dimCount = MdimArrayElementCount.Aggregate(0, (acc, act) => acc += (act > 0) ? 1 : 0); + if (dimCount != indexes.Count()) throw new Exception("Out of bounds"); + // check bounds + for (int i = 0; i < dimCount; ++i) + { + indexes[i] = (indexes[i] - MdimArrayLowerBounds[dimCount - i - 1]); + if (indexes[i] > MdimArrayElementCount[dimCount - i - 1]) throw new Exception("Out of bounds"); + if (indexes[i] < 0) throw new Exception("Out of bounds"); + } + + // calc dim size + if (varType.Softdatatype == Softdatatype.S7COMMP_SOFTDATATYPE_BBOOL) + { + MdimArrayElementCount[0] += 8 - MdimArrayElementCount[0] % 8; // for bool must be a mutiple of 8! + } + uint[] dimSize = new uint[dimCount]; + uint g = 1; + for (int i = 0; i < dimCount - 1; ++i) + { + dimSize[i] = g; + g *= MdimArrayElementCount[i]; + } + dimSize[dimCount - 1] = g; + + // calc id + int arrayIndex = 0; + for (int i = 0; i < dimCount; ++i) + { + arrayIndex += indexes[i] * (int)dimSize[dimCount - i - 1]; + } + + varInfo.AccessSequence += "." + String.Format("{0:X}", arrayIndex); + if (varType.OffsetInfoType.HasRelation()) varInfo.AccessSequence += ".1"; // additional ".1" for array of struct + } + + /// + /// Browses the symbol level by level recursively. Fetches missing type info automatically from the plc. + /// + /// type info relid + /// plc tag symbol + /// used to build access sequence + /// plc tag or null if not found + /// Symbol syntax error, Out of bounds + private PlcTag browsePlcTagBySymbol(uint ti_relid, ref string symbol, VarInfo varInfo) + { + PObject pObj = getTypeInfoByRelId(ti_relid); + if (pObj == null) throw new Exception("Could not get type info"); + string levelName = parseSymbolLevel(ref symbol); + // find level name of symbol in var list + int idx = pObj.VarnameList?.Names?.IndexOf(levelName) ?? -1; + if (idx < 0) return null; + PVartypeListElement varType = pObj.VartypeList.Elements[idx]; + varInfo.AccessSequence += "." + String.Format("{0:X}", varType.LID); + bool is1Dim = false; + if (varType.OffsetInfoType.Is1Dim()) + { + if (symbol == "") + { + is1Dim = true; + } + else + { + calcAccessSeqFor1DimArray(ref symbol, varType, varInfo); + } + } + if (varType.OffsetInfoType.IsMDim()) + { + calcAccessSeqForMDimArray(ref symbol, varType, varInfo); + } + if (varType.OffsetInfoType.HasRelation()) + { + if (symbol.Length <= 0 && varType.Softdatatype == Softdatatype.S7COMMP_SOFTDATATYPE_DTL) + { + return PlcTags.TagFactory(varInfo.Name, new ItemAddress(varInfo.AccessSequence), varType.Softdatatype, is1Dim); + } + if (symbol.Length <= 0) + { + return null; + } + else + { + var ioit = (IOffsetInfoType_Relation)varType.OffsetInfoType; + return browsePlcTagBySymbol(ioit.GetRelationId(), ref symbol, varInfo); + } + } + else + { + return PlcTags.TagFactory(varInfo.Name, new ItemAddress(varInfo.AccessSequence), varType.Softdatatype, is1Dim); + } + } + + /// + /// Get the plc tag for the given plc tag symbol. + /// + /// plc tag symbol + /// plc tag, returns null if plc tag could not be found + public PlcTag getPlcTagBySymbol(string symbol) + { + VarInfo varInfo = new VarInfo(); + varInfo.Name = symbol; + // make sure we have the db list + if (dbInfoList == null) + { + if (GetListOfDatablocks(out dbInfoList) != 0) { return null; } + } + string levelName = parseSymbolLevel(ref symbol); + // find db by first level name of symbol + DatablockInfo dbInfo = dbInfoList.Find(dbi => dbi.db_name == levelName); + if (dbInfo != null) + { + varInfo.AccessSequence = String.Format("{0:X}", dbInfo.db_block_relid); + return browsePlcTagBySymbol(dbInfo.db_block_ti_relid, ref symbol, varInfo); + } + else + { + symbol = varInfo.Name; + // Merker + varInfo.AccessSequence = String.Format("{0:X}", Ids.NativeObjects_theMArea_Rid); + PlcTag tag = browsePlcTagBySymbol(0x90030000, ref symbol, varInfo); + if (tag != null) return tag; + symbol = varInfo.Name; + // Outputs + varInfo.AccessSequence = String.Format("{0:X}", Ids.NativeObjects_theQArea_Rid); + tag = browsePlcTagBySymbol(0x90020000, ref symbol, varInfo); + if (tag != null) return tag; + symbol = varInfo.Name; + // Inputs + varInfo.AccessSequence = String.Format("{0:X}", Ids.NativeObjects_theIArea_Rid); + tag = browsePlcTagBySymbol(0x90010000, ref symbol, varInfo); + if (tag != null) return tag; + // TODO: implement s5timers and counters... no one uses them anymore anyway + } + return null; + } + + public class BrowseEntry + { + public string Name; + public uint Softdatatype; + public UInt32 LID; + public UInt32 SymbolCrc; + public string AccessSequence; + }; + + public class BrowseData + { + public string db_name; // Name of the datablock + public UInt32 db_number; // Number of the datablock + public UInt32 db_block_relid; // RID of the datablock + public UInt32 db_block_ti_relid; // Type-Info RID of the datablock + public List variables = new List(); // Variables inside the datablock + }; + + public class DatablockInfo + { + public string db_name; // Name of the datablock + public UInt32 db_number; // Number of the datablock + public UInt32 db_block_relid; // RID of the datablock + public UInt32 db_block_ti_relid; // Type-Info RID of the datablock + }; + + public int GetListOfDatablocks(out List dbInfoList) + { + int res; + + dbInfoList = new List(); + + var exploreReq = new ExploreRequest(ProtocolVersion.V2); + exploreReq.ExploreId = Ids.NativeObjects_thePLCProgram_Rid; + exploreReq.ExploreRequestId = Ids.None; + exploreReq.ExploreChildsRecursive = 1; + exploreReq.ExploreParents = 0; + + // Add the attributes we need in the response + exploreReq.AddressList.Add(Ids.ObjectVariableTypeName); + + // Set filter on Id for Datablock Class RID. With this filter, we only + // get informations from datablocks, and not other blocks we don't need here. + var filter = new ValueStruct(Ids.Filter); + filter.AddStructElement(Ids.FilterOperation, new ValueDInt(8)); // 8 = InstanceIOf + filter.AddStructElement(Ids.AddressCount, new ValueUDInt(0)); + uint[] faddress = new uint[32]; // Unknown, possible dependant on FilterOperation + filter.AddStructElement(Ids.Address, new ValueUDIntArray(faddress)); + filter.AddStructElement(Ids.FilterValue, new ValueRID(Ids.DB_Class_Rid)); + + exploreReq.FilterData = filter; + + res = SendS7plusFunctionObject(exploreReq); + if (res != 0) + { + return res; + } + m_LastError = 0; + WaitForNewS7plusReceived(m_ReadTimeout); + if (m_LastError != 0) + { + return m_LastError; + } + + var exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); + res = checkResponseWithIntegrity(exploreReq, exploreRes); + if (res != 0) + { + return res; + } + + // Get the datablock information we want further informations from. + var objList = exploreRes.Objects; + + foreach (var ob in objList) + { + // May be this check can be removed, if setting the filter to the DB_Class_Rid is working 100%. + switch (ob.ClassId) + { + case Ids.DB_Class_Rid: + UInt32 relid = ob.RelationId; + UInt32 area = (relid >> 16); + UInt32 num = relid & 0xffff; + if (area == 0x8a0e) + { + var name = (ValueWString)(ob.GetAttribute(Ids.ObjectVariableTypeName)); + DatablockInfo data = new DatablockInfo(); + data.db_block_relid = relid; + data.db_name = name.GetValue(); + data.db_number = num; + dbInfoList.Add(data); + } + break; + } + } + + // Get the TypeInfo RID to RelId from the first response + + // With LID=1 we get the RID back. With this number we can explore further + // informations of this datablock. + // This is neccessary, because informations about instance DBs (e.g. TON) you + // don't get by the RID of the DB, instead of exploring the TON Type RID. + var readlist = new List(); + var values = new List(); + var errors = new List(); + + foreach (var data in dbInfoList) + { + if (data.db_number > 0) + { + // Insert the address + var adr1 = new ItemAddress(); + adr1.AccessArea = data.db_block_relid; + adr1.AccessSubArea = Ids.DB_ValueActual; + adr1.LID.Add(1); + readlist.Add(adr1); + } + } + res = ReadValues(readlist, out values, out errors); + if (res != 0) + { + return res; + } + + // Insert response data into the list + for (int i = 0; i < values.Count; i++) + { + if (errors[i] == 0) + { + var rid = (ValueRID)values[i]; + var data = dbInfoList[i]; + data.db_block_ti_relid = rid.GetValue(); + dbInfoList[i] = data; + } + else + { + // On error, set relid=0, which is then removed in the next step. + // Should we report this for the user? + var data = dbInfoList[i]; + data.db_block_ti_relid = 0; + dbInfoList[i] = data; + } + } + + // Remove elements with db_block_ti_relid == 0. + // This can occur on datablocks which are only in load memory and can't be explored. + dbInfoList.RemoveAll(item => item.db_block_ti_relid == 0); + + return 0; + } + + public int GetTypeInformation(uint exploreId, out List objList) + { + int res; + objList = new List(); + + var exploreReq = new ExploreRequest(ProtocolVersion.V2); + exploreReq.ExploreId = exploreId; + exploreReq.ExploreRequestId = Ids.None; + exploreReq.ExploreChildsRecursive = 1; + exploreReq.ExploreParents = 0; + + res = SendS7plusFunctionObject(exploreReq); + if (res != 0) + { + return res; + } + m_LastError = 0; + WaitForNewS7plusReceived(m_ReadTimeout); + if (m_LastError != 0) + { + return m_LastError; + } + + var exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); + res = checkResponseWithIntegrity(exploreReq, exploreRes); + if (res != 0) + { + return res; + } + objList = exploreRes.Objects; + + return 0; + } + + /// + /// Requests the tag and block comments from the Plc, returned as XML strings. + /// xml_linecomment: + /// The returned XML format differs between between request of I/Q/M/C/T areas and datablocks: + /// I/Q/M/C/T: .... + /// Datablock: .... + /// As "ID" the number for the variable identification is used. + /// + /// xml_dbcomment: + /// The xml-value description generated from our own value xml-serialization for WStringSparseArray. The value key is the language id. + /// Example: + /// DB Kommentar in german de-DEDB comment in english en-US + /// + /// The relation ID for the area you want the comments for, e.g. 0x8a0e0000+db_number, or 0x52 for M-area + /// + /// + /// 0 if no error + public int GetCommentsXml(uint relid, out string xml_linecomment, out string xml_dbcomment) + { + int res; + // With requesting DataInterface_InterfaceDescription, whe would be able to get all informations like the access ids and + // datatype informations, that we get from the other browsing method. Needs to be tested which one is more efficient on network traffic or plc load. + // If we keep use browsing for the comments, at least we would be able to read all information in one request. + xml_linecomment = String.Empty; + xml_dbcomment = String.Empty; + + var exploreReq = new ExploreRequest(ProtocolVersion.V2); + exploreReq.ExploreId = relid; + exploreReq.ExploreRequestId = Ids.None; + exploreReq.ExploreChildsRecursive = 1; + exploreReq.ExploreParents = 0; + + // We want to know the following attributes + exploreReq.AddressList.Add(Ids.ASObjectES_Comment); + exploreReq.AddressList.Add(Ids.DataInterface_LineComments); + + res = SendS7plusFunctionObject(exploreReq); + if (res != 0) + { + return res; + } + m_LastError = 0; + WaitForNewS7plusReceived(m_ReadTimeout); + if (m_LastError != 0) + { + return m_LastError; + } + + var exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); + res = checkResponseWithIntegrity(exploreReq, exploreRes); + if (res != 0) + { + return res; + } + + foreach (var obj in exploreRes.Objects) + { + foreach (var att in obj.Attributes) + { + switch (att.Key) + { + case Ids.ASObjectES_Comment: + var att_comment = (ValueWStringSparseArray)att.Value; + xml_dbcomment = att_comment.ToString(); + break; + case Ids.DataInterface_LineComments: + var att_linecomment = (ValueBlobSparseArray)att.Value; + BlobDecompressor bd = new BlobDecompressor(); + var blob_sp = att_linecomment.GetValue(); + // In DBs we get the data with Sparsearray key = 1, in M-Area with key = 2. + // For now, just take the first, don't know where the key ids are for. + foreach (var key in blob_sp.Keys) + { + xml_linecomment = bd.decompress(blob_sp[key].value, 4); // Offset of 4, as we have a header for the zlib dictionary version + break; + } + break; + } + } + } + return 0; + } + } + #endregion +} diff --git a/src/S7CommPlusDriver/S7CommPlusConnectionHighLevel.cs b/src/S7CommPlusDriver/S7CommPlusConnectionHighLevel.cs index 3cf52c8..6a1bb04 100644 --- a/src/S7CommPlusDriver/S7CommPlusConnectionHighLevel.cs +++ b/src/S7CommPlusDriver/S7CommPlusConnectionHighLevel.cs @@ -1,37 +1,37 @@ -#region License -/****************************************************************************** - * S7CommPlusDriver - * - * Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de - * - * This file is part of S7CommPlusDriver. - * - * S7CommPlusDriver is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - /****************************************************************************/ -#endregion - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace S7CommPlusDriver -{ - public partial class S7CommPlusConnection - { - public enum BlockType - { - unkown, - DB, - FB, - FC, - OB, - UDT, - } - +#region License +/****************************************************************************** + * S7CommPlusDriver + * + * Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de + * + * This file is part of S7CommPlusDriver. + * + * S7CommPlusDriver is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + /****************************************************************************/ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace S7CommPlusDriver +{ + public partial class S7CommPlusConnection + { + public enum BlockType + { + unkown, + DB, + FB, + FC, + OB, + UDT, + } + public enum ProgrammingLanguage : int { Undef = 0, @@ -88,8 +88,8 @@ public enum ProgrammingLanguage : int ProDiag = 500, ProDiag_OB = 501, CEM = 600, - } - + } + public enum BinaryArtifacts : uint { Undefined = 0u, @@ -105,141 +105,141 @@ public enum BinaryArtifacts : uint VirtualPlcMc7plusDataKey = 2147483682u, VirtualPlcOptimizationInfoDataKey = 2147483683u, VirtualPlcClosedImmediateDataKey = 2147483684u - } - - public class BlockInfo - { - public string name; // Name of the datablock - public UInt32 number; // Number of the datablock - public BlockType type; + } + + public class BlockInfo + { + public string name; // Name of the datablock + public UInt32 number; // Number of the datablock + public BlockType type; public ProgrammingLanguage lang; - public UInt32 db_block_relid; // RID of the datablock - public UInt32 db_block_ti_relid; // Type-Info RID of the datablock - }; - - public int BrowseAllBlocks(out List exploreData) - { - int res; - Browser vars = new Browser(); - ExploreRequest exploreReq; - ExploreResponse exploreRes; - - #region Read all objects - - exploreData = new List(); - - exploreReq = new ExploreRequest(ProtocolVersion.V2); - exploreReq.ExploreId = Ids.NativeObjects_thePLCProgram_Rid; - exploreReq.ExploreRequestId = Ids.None; - exploreReq.ExploreChildsRecursive = 1; - exploreReq.ExploreParents = 0; - - // We want to know the following attributes - exploreReq.AddressList.Add(Ids.ObjectVariableTypeName); - exploreReq.AddressList.Add(Ids.Block_BlockNumber); - exploreReq.AddressList.Add(Ids.Block_BlockLanguage); - - res = SendS7plusFunctionObject(exploreReq); - if (res != 0) - { - return res; - } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - return m_LastError; - } - - exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); - res = checkResponseWithIntegrity(exploreReq, exploreRes); - if (res != 0) - { - return res; - } - - #endregion - - #region Evaluate all data blocks that then need to be browsed - - var obj = exploreRes.Objects.First(o => o.ClassId == Ids.PLCProgram_Class_Rid); - foreach (var ob in obj.GetObjects()) - { - switch (ob.ClassId) - { - case Ids.DB_Class_Rid: - case Ids.FB_Class_Rid: + public UInt32 db_block_relid; // RID of the datablock + public UInt32 db_block_ti_relid; // Type-Info RID of the datablock + }; + + public int BrowseAllBlocks(out List exploreData) + { + int res; + Browser vars = new Browser(); + ExploreRequest exploreReq; + ExploreResponse exploreRes; + + #region Read all objects + + exploreData = new List(); + + exploreReq = new ExploreRequest(ProtocolVersion.V2); + exploreReq.ExploreId = Ids.NativeObjects_thePLCProgram_Rid; + exploreReq.ExploreRequestId = Ids.None; + exploreReq.ExploreChildsRecursive = 1; + exploreReq.ExploreParents = 0; + + // We want to know the following attributes + exploreReq.AddressList.Add(Ids.ObjectVariableTypeName); + exploreReq.AddressList.Add(Ids.Block_BlockNumber); + exploreReq.AddressList.Add(Ids.Block_BlockLanguage); + + res = SendS7plusFunctionObject(exploreReq); + if (res != 0) + { + return res; + } + m_LastError = 0; + WaitForNewS7plusReceived(m_ReadTimeout); + if (m_LastError != 0) + { + return m_LastError; + } + + exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); + res = checkResponseWithIntegrity(exploreReq, exploreRes); + if (res != 0) + { + return res; + } + + #endregion + + #region Evaluate all data blocks that then need to be browsed + + var obj = exploreRes.Objects.First(o => o.ClassId == Ids.PLCProgram_Class_Rid); + foreach (var ob in obj.GetObjects()) + { + switch (ob.ClassId) + { + case Ids.DB_Class_Rid: + case Ids.FB_Class_Rid: case Ids.FC_Class_Rid: - case Ids.OB_Class_Rid: - UInt32 relid = ob.RelationId; - UInt32 area = (relid >> 16); - UInt32 num = relid & 0xffff; - - var name = (ValueWString)(ob.GetAttribute(Ids.ObjectVariableTypeName)); - var data = new BlockInfo(); - data.db_block_relid = relid; - data.name = name.GetValue(); - data.number = num; - data.type = ob.ClassId switch - { - Ids.DB_Class_Rid => BlockType.DB, - Ids.FB_Class_Rid => BlockType.FB, - Ids.FC_Class_Rid => BlockType.FC, - Ids.OB_Class_Rid => BlockType.OB, - }; - - var lang = ((ValueUInt)ob.Attributes[Ids.Block_BlockLanguage]).GetValue(); - data.lang = (ProgrammingLanguage)lang; - exploreData.Add(data); - break; - } - } - - #endregion - - return 0; + case Ids.OB_Class_Rid: + UInt32 relid = ob.RelationId; + UInt32 area = (relid >> 16); + UInt32 num = relid & 0xffff; + + var name = (ValueWString)(ob.GetAttribute(Ids.ObjectVariableTypeName)); + var data = new BlockInfo(); + data.db_block_relid = relid; + data.name = name.GetValue(); + data.number = num; + data.type = ob.ClassId switch + { + Ids.DB_Class_Rid => BlockType.DB, + Ids.FB_Class_Rid => BlockType.FB, + Ids.FC_Class_Rid => BlockType.FC, + Ids.OB_Class_Rid => BlockType.OB, + }; + + var lang = ((ValueUInt)ob.Attributes[Ids.Block_BlockLanguage]).GetValue(); + data.lang = (ProgrammingLanguage)lang; + exploreData.Add(data); + break; + } + } + + #endregion + + return 0; } - public int GetPlcStructureXML(out string plcStrutureXml) - { - int res; - Browser vars = new Browser(); - ExploreRequest exploreReq; - ExploreResponse exploreRes; - plcStrutureXml = null; - - #region Read all objects - - exploreReq = new ExploreRequest(ProtocolVersion.V2); - exploreReq.ExploreId = Ids.Constants | 0x0000ffff; - exploreReq.ExploreRequestId = Ids.None; - exploreReq.ExploreChildsRecursive = 1; - exploreReq.ExploreParents = 1; - - exploreReq.AddressList.Add(Ids.ConstantsGlobal_Symbolics); - - res = SendS7plusFunctionObject(exploreReq); - if (res != 0) - { - return res; - } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - return m_LastError; - } - - exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); - res = checkResponseWithIntegrity(exploreReq, exploreRes); - if (res != 0) - { - return res; - } - - #endregion - - var attr = exploreRes?.Objects?[0]?.Objects?.First().Value?.Objects?.First().Value?.Attributes?[Ids.ConstantsGlobal_Symbolics] as ValueBlob; + public int GetPlcStructureXML(out string plcStrutureXml) + { + int res; + Browser vars = new Browser(); + ExploreRequest exploreReq; + ExploreResponse exploreRes; + plcStrutureXml = null; + + #region Read all objects + + exploreReq = new ExploreRequest(ProtocolVersion.V2); + exploreReq.ExploreId = Ids.Constants | 0x0000ffff; + exploreReq.ExploreRequestId = Ids.None; + exploreReq.ExploreChildsRecursive = 1; + exploreReq.ExploreParents = 1; + + exploreReq.AddressList.Add(Ids.ConstantsGlobal_Symbolics); + + res = SendS7plusFunctionObject(exploreReq); + if (res != 0) + { + return res; + } + m_LastError = 0; + WaitForNewS7plusReceived(m_ReadTimeout); + if (m_LastError != 0) + { + return m_LastError; + } + + exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); + res = checkResponseWithIntegrity(exploreReq, exploreRes); + if (res != 0) + { + return res; + } + + #endregion + + var attr = exploreRes?.Objects?[0]?.Objects?.First().Value?.Objects?.First().Value?.Attributes?[Ids.ConstantsGlobal_Symbolics] as ValueBlob; if (attr != null) { BlobDecompressor bd3 = new BlobDecompressor(); @@ -247,66 +247,66 @@ public int GetPlcStructureXML(out string plcStrutureXml) var xml = bd3.decompress(v, 0); plcStrutureXml = xml; } - - return 0; - } - - public int GetBlockXml(uint relid, out string blockName, out ProgrammingLanguage lang, out uint blockNumber, out BlockType blockType, out string xml_linecomment, out Dictionary xml_comment, out string interfaceDescription, out string[] blockBody, out string fuctionalObjectCode, out string[] intRef, out string[] extRef) - { - int res; - // With requesting DataInterface_InterfaceDescription, whe would be able to get all informations like the access ids and - // datatype informations, that we get from the other browsing method. Needs to be tested which one is more efficient on network traffic or plc load. - // If we keep use browsing for the comments, at least we would be able to read all information in one request. - xml_linecomment = String.Empty; - xml_comment = new(); - interfaceDescription = String.Empty; - blockBody = new string[0]; - fuctionalObjectCode = String.Empty; - intRef = new string[0]; - extRef = new string[0]; - blockName = null; - lang = ProgrammingLanguage.Undef; - blockNumber = relid & 0xffff; - blockType = BlockType.unkown; - - var exploreReq = new ExploreRequest(ProtocolVersion.V2); - exploreReq.ExploreId = relid; - exploreReq.ExploreRequestId = Ids.None; - exploreReq.ExploreChildsRecursive = 1; + + return 0; + } + + public int GetBlockXml(uint relid, out string blockName, out ProgrammingLanguage lang, out uint blockNumber, out BlockType blockType, out string xml_linecomment, out Dictionary xml_comment, out string interfaceDescription, out string[] blockBody, out string fuctionalObjectCode, out string[] intRef, out string[] extRef) + { + int res; + // With requesting DataInterface_InterfaceDescription, whe would be able to get all informations like the access ids and + // datatype informations, that we get from the other browsing method. Needs to be tested which one is more efficient on network traffic or plc load. + // If we keep use browsing for the comments, at least we would be able to read all information in one request. + xml_linecomment = String.Empty; + xml_comment = new(); + interfaceDescription = String.Empty; + blockBody = new string[0]; + fuctionalObjectCode = String.Empty; + intRef = new string[0]; + extRef = new string[0]; + blockName = null; + lang = ProgrammingLanguage.Undef; + blockNumber = relid & 0xffff; + blockType = BlockType.unkown; + + var exploreReq = new ExploreRequest(ProtocolVersion.V2); + exploreReq.ExploreId = relid; + exploreReq.ExploreRequestId = Ids.None; + exploreReq.ExploreChildsRecursive = 1; exploreReq.ExploreParents = 0; // We want to know the following attributes - exploreReq.AddressList.Add(Ids.ObjectVariableTypeName); - //exploreReq.AddressList.Add(Ids.Block_BlockNumber); - exploreReq.AddressList.Add(Ids.Block_BlockLanguage); - - exploreReq.AddressList.Add(Ids.ASObjectES_Comment); - exploreReq.AddressList.Add(Ids.DataInterface_LineComments); - exploreReq.AddressList.Add(Ids.DataInterface_InterfaceDescription); - exploreReq.AddressList.Add(Ids.Block_BodyDescription); + exploreReq.AddressList.Add(Ids.ObjectVariableTypeName); + //exploreReq.AddressList.Add(Ids.Block_BlockNumber); + exploreReq.AddressList.Add(Ids.Block_BlockLanguage); + + exploreReq.AddressList.Add(Ids.ASObjectES_Comment); + exploreReq.AddressList.Add(Ids.DataInterface_LineComments); + exploreReq.AddressList.Add(Ids.DataInterface_InterfaceDescription); + exploreReq.AddressList.Add(Ids.Block_BodyDescription); exploreReq.AddressList.Add(Ids.FunctionalObject_extRefData); exploreReq.AddressList.Add(Ids.FunctionalObject_intRefData); - res = SendS7plusFunctionObject(exploreReq); - if (res != 0) - { - return res; - } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - return m_LastError; - } - - var exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); - res = checkResponseWithIntegrity(exploreReq, exploreRes); - if (res != 0) - { - return res; - } - - foreach (var obj in exploreRes.Objects) + res = SendS7plusFunctionObject(exploreReq); + if (res != 0) + { + return res; + } + m_LastError = 0; + WaitForNewS7plusReceived(m_ReadTimeout); + if (m_LastError != 0) + { + return m_LastError; + } + + var exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); + res = checkResponseWithIntegrity(exploreReq, exploreRes); + if (res != 0) + { + return res; + } + + foreach (var obj in exploreRes.Objects) { blockType = obj.ClassId switch { @@ -337,151 +337,151 @@ public int GetBlockXml(uint relid, out string blockName, out ProgrammingLanguage 2657 => BlockType.OB, // TimeOfDayOB 2658 => BlockType.OB, // UpdateEventOB 8440 => BlockType.OB, // LookAheadOB - }; - - - foreach (var att in obj.Attributes) - { - switch (att.Key) + }; + + + foreach (var att in obj.Attributes) + { + switch (att.Key) { - case Ids.ObjectVariableTypeName: + case Ids.ObjectVariableTypeName: { blockName = ((ValueWString)att.Value).GetValue(); break; } - case Ids.Block_BlockNumber: + case Ids.Block_BlockNumber: { break; } - case Ids.Block_BlockLanguage: + case Ids.Block_BlockLanguage: { var l = ((ValueUInt)att.Value).GetValue(); lang = (ProgrammingLanguage)l; break; - } - case Ids.FunctionalObject_extRefData: - { - var xx = (ValueBlobSparseArray)att.Value; - BlobDecompressor bd3 = new BlobDecompressor(); - var blob_sp3 = xx.GetValue(); - extRef = new string[blob_sp3.Count]; - var i = 0; - foreach (var key in blob_sp3.Keys) - { - if (blob_sp3[key].value != null) - extRef[i++] = bd3.decompress(blob_sp3[key].value, 4); - } - break; } - case Ids.FunctionalObject_intRefData: - { - var xx = (ValueBlobSparseArray)att.Value; - BlobDecompressor bd3 = new BlobDecompressor(); - var blob_sp3 = xx.GetValue(); - intRef = new string[blob_sp3.Count]; - var i = 0; - foreach (var key in blob_sp3.Keys) - { - if (blob_sp3[key].value != null) - intRef[i++] = bd3.decompress(blob_sp3[key].value, 4); - } - break; - } - case Ids.ASObjectES_Comment: - { - var att_comment = (ValueWStringSparseArray)att.Value; - xml_comment = att_comment.GetValue(); - break; - } - case Ids.DataInterface_LineComments: - { - var att_linecomment = (ValueBlobSparseArray)att.Value; - BlobDecompressor bd = new BlobDecompressor(); - var blob_sp = att_linecomment.GetValue(); - // In DBs we get the data with Sparsearray key = 1, in M-Area with key = 2. - // For now, just take the first, don't know where the key ids are for. - foreach (var key in blob_sp.Keys) - { - xml_linecomment = bd.decompress(blob_sp[key].value, 4); // Offset of 4, as we have a header for the zlib dictionary version - break; - } - break; - } - case Ids.DataInterface_InterfaceDescription: - { - var att_ifsescr = (ValueBlob)att.Value; - BlobDecompressor bd2 = new BlobDecompressor(); - var blob_sp2 = att_ifsescr.GetValue(); - interfaceDescription = bd2.decompress(blob_sp2, 4); // Offset of 4, as we have a header for the zlib dictionary version - break; - } - case Ids.Block_BodyDescription: - { - var xx = (ValueBlobSparseArray)att.Value; - BlobDecompressor bd3 = new BlobDecompressor(); - var blob_sp3 = xx.GetValue(); - blockBody = new string[blob_sp3.Where(x => x.Key < (uint)BinaryArtifacts.PlcFamily).Count()]; - var i = 0; - foreach (var key in blob_sp3.Keys.OrderBy(x => x)) - { + case Ids.FunctionalObject_extRefData: + { + var xx = (ValueBlobSparseArray)att.Value; + BlobDecompressor bd3 = new BlobDecompressor(); + var blob_sp3 = xx.GetValue(); + extRef = new string[blob_sp3.Count]; + var i = 0; + foreach (var key in blob_sp3.Keys) + { + if (blob_sp3[key].value != null) + extRef[i++] = bd3.decompress(blob_sp3[key].value, 4); + } + break; + } + case Ids.FunctionalObject_intRefData: + { + var xx = (ValueBlobSparseArray)att.Value; + BlobDecompressor bd3 = new BlobDecompressor(); + var blob_sp3 = xx.GetValue(); + intRef = new string[blob_sp3.Count]; + var i = 0; + foreach (var key in blob_sp3.Keys) + { + if (blob_sp3[key].value != null) + intRef[i++] = bd3.decompress(blob_sp3[key].value, 4); + } + break; + } + case Ids.ASObjectES_Comment: + { + var att_comment = (ValueWStringSparseArray)att.Value; + xml_comment = att_comment.GetValue(); + break; + } + case Ids.DataInterface_LineComments: + { + var att_linecomment = (ValueBlobSparseArray)att.Value; + BlobDecompressor bd = new BlobDecompressor(); + var blob_sp = att_linecomment.GetValue(); + // In DBs we get the data with Sparsearray key = 1, in M-Area with key = 2. + // For now, just take the first, don't know where the key ids are for. + foreach (var key in blob_sp.Keys) + { + xml_linecomment = bd.decompress(blob_sp[key].value, 4); // Offset of 4, as we have a header for the zlib dictionary version + break; + } + break; + } + case Ids.DataInterface_InterfaceDescription: + { + var att_ifsescr = (ValueBlob)att.Value; + BlobDecompressor bd2 = new BlobDecompressor(); + var blob_sp2 = att_ifsescr.GetValue(); + interfaceDescription = bd2.decompress(blob_sp2, 4); // Offset of 4, as we have a header for the zlib dictionary version + break; + } + case Ids.Block_BodyDescription: + { + var xx = (ValueBlobSparseArray)att.Value; + BlobDecompressor bd3 = new BlobDecompressor(); + var blob_sp3 = xx.GetValue(); + blockBody = new string[blob_sp3.Where(x => x.Key < (uint)BinaryArtifacts.PlcFamily).Count()]; + var i = 0; + foreach (var key in blob_sp3.Keys.OrderBy(x => x)) + { if (!(key < (uint)BinaryArtifacts.PlcFamily)) { //TODO: what to do with binary artifacts? var binaryArtifactType = (BinaryArtifacts)key; continue; - } - + } + if (blob_sp3[key].value != null) { var code = bd3.decompress(blob_sp3[key].value, 4); blockBody[i++] = code; - } - } - break; - } - } - } - } - return 0; + } + } + break; + } + } + } + } + return 0; } - public int RunExploreRequest(uint relid, uint[] attributes, out List objects, byte exploreChildsRecursive = 1, byte exploreParents = 0) + public int RunExploreRequest(uint relid, uint[] attributes, out List objects, byte exploreChildsRecursive = 1, byte exploreParents = 0) { int res; - objects = null; - - var exploreReq = new ExploreRequest(ProtocolVersion.V2); - exploreReq.ExploreId = relid; - exploreReq.ExploreRequestId = Ids.None; - exploreReq.ExploreChildsRecursive = exploreChildsRecursive; - exploreReq.ExploreParents = exploreParents; - + objects = null; + + var exploreReq = new ExploreRequest(ProtocolVersion.V2); + exploreReq.ExploreId = relid; + exploreReq.ExploreRequestId = Ids.None; + exploreReq.ExploreChildsRecursive = exploreChildsRecursive; + exploreReq.ExploreParents = exploreParents; + if (attributes != null && attributes.Length > 0) { // We want to know the following attributes exploreReq.AddressList.AddRange(attributes); - } - - res = SendS7plusFunctionObject(exploreReq); - if (res != 0) - { - return res; - } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - return m_LastError; - } - - var exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); - res = checkResponseWithIntegrity(exploreReq, exploreRes); - if (res != 0) - { - return res; - } - + } + + res = SendS7plusFunctionObject(exploreReq); + if (res != 0) + { + return res; + } + m_LastError = 0; + WaitForNewS7plusReceived(m_ReadTimeout); + if (m_LastError != 0) + { + return m_LastError; + } + + var exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); + res = checkResponseWithIntegrity(exploreReq, exploreRes); + if (res != 0) + { + return res; + } + objects = exploreRes.Objects; return 0; @@ -491,17 +491,17 @@ public int RunGetVarSubstreamedRequest(uint objectId, ushort address, out PValue { int res; value = null; - + var getVarSubstreamedReq = new GetVarSubstreamedRequest(ProtocolVersion.V2); - getVarSubstreamedReq.InObjectId = objectId; - getVarSubstreamedReq.SessionId = m_SessionId; - getVarSubstreamedReq.Address = address; - res = SendS7plusFunctionObject(getVarSubstreamedReq); - if (res != 0) - { - return res; + getVarSubstreamedReq.InObjectId = objectId; + getVarSubstreamedReq.SessionId = m_SessionId; + getVarSubstreamedReq.Address = address; + res = SendS7plusFunctionObject(getVarSubstreamedReq); + if (res != 0) + { + return res; } - m_LastError = 0; + m_LastError = 0; WaitForNewS7plusReceived(m_ReadTimeout); if (m_LastError != 0) { @@ -509,10 +509,10 @@ public int RunGetVarSubstreamedRequest(uint objectId, ushort address, out PValue } - var getVarSubstreamedRes = GetVarSubstreamedResponse.DeserializeFromPdu(m_ReceivedPDU); - if (getVarSubstreamedRes == null) - { - return S7Consts.errIsoInvalidPDU8; + var getVarSubstreamedRes = GetVarSubstreamedResponse.DeserializeFromPdu(m_ReceivedPDU); + if (getVarSubstreamedRes == null) + { + return S7Consts.errIsoInvalidPDU8; } value = getVarSubstreamedRes.Value; @@ -579,5 +579,5 @@ public int GetCpuInfos(out CpuInfo cpuInfo) return 0; } - } -} + } +}