From 5330593256047be8f6aa6b41ad2a35362ab8157e Mon Sep 17 00:00:00 2001 From: SigiRab Date: Fri, 14 Nov 2025 11:15:37 +0100 Subject: [PATCH 01/13] =?UTF-8?q?bitte=20die=20neue=20S7=201200=20G2=20ein?= =?UTF-8?q?f=C3=BCgen,=20diese=20Serie=20kann=20ab=20Tia=20V20=20programmi?= =?UTF-8?q?ert=20werden=20und=20unterst=C3=BCtz=20keine=20Legacy-Verbindun?= =?UTF-8?q?gen=20mehr.=20Der=20Treiber=20funktioniert=20aber=20ansonsten?= =?UTF-8?q?=20einwandfrei.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/S7CommPlusDriver/Legitimation/Legitimation.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/S7CommPlusDriver/Legitimation/Legitimation.cs b/src/S7CommPlusDriver/Legitimation/Legitimation.cs index 41e97c7..5645459 100644 --- a/src/S7CommPlusDriver/Legitimation/Legitimation.cs +++ b/src/S7CommPlusDriver/Legitimation/Legitimation.cs @@ -51,6 +51,10 @@ private int legitimate(ValueStruct serverSession, string password, string userna legacyLegitimation = true; } } + 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")) { if (fwVerNo < 403) From 38b00f2bd252c14e692af91b9dde6fc930d1f288 Mon Sep 17 00:00:00 2001 From: Thomas Wiens Date: Thu, 27 Nov 2025 20:07:58 +0100 Subject: [PATCH 02/13] Use double quote for string --- src/S7CommPlusDriver/Legitimation/Legitimation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/S7CommPlusDriver/Legitimation/Legitimation.cs b/src/S7CommPlusDriver/Legitimation/Legitimation.cs index 5645459..8050fd5 100644 --- a/src/S7CommPlusDriver/Legitimation/Legitimation.cs +++ b/src/S7CommPlusDriver/Legitimation/Legitimation.cs @@ -51,7 +51,7 @@ private int legitimate(ValueStruct serverSession, string password, string userna legacyLegitimation = true; } } - else if (sessionVersionPAOMString.Contains("50-0XB0") && deviceVersion.StartsWith('2')) //New S7-1200 G2 (example: "1;6ES7 212-1HG50-0XB0;V1.0") + else if (sessionVersionPAOMString.Contains("50-0XB0") && deviceVersion.StartsWith("2")) //New S7-1200 G2 (example: "1;6ES7 212-1HG50-0XB0;V1.0") { legacyLegitimation = false; } From 9f1864f7a2b879623241d94ce14dd22796dc6929 Mon Sep 17 00:00:00 2001 From: Thomas Wiens Date: Thu, 27 Nov 2025 21:08:51 +0100 Subject: [PATCH 03/13] Alarms: Add method to read the active alarms without notification --- src/S7CommPlusDriver/Alarming/BrowseAlarms.cs | 68 +++++++++++++++++++ src/S7CommPlusDriver/Core/Ids.cs | 2 + 2 files changed, 70 insertions(+) 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/Core/Ids.cs b/src/S7CommPlusDriver/Core/Ids.cs index f543bf7..12b330e 100644 --- a/src/S7CommPlusDriver/Core/Ids.cs +++ b/src/S7CommPlusDriver/Core/Ids.cs @@ -19,6 +19,7 @@ public static class Ids { public const int None = 0; public const int NativeObjects_thePLCProgram_Rid = 3; + public const int NativeObjects_theAlarmSubsystem_Rid = 8; public const int NativeObjects_theCPUexecUnit_Rid = 52; public const int NativeObjects_theIArea_Rid = 80; public const int NativeObjects_theQArea_Rid = 81; @@ -86,6 +87,7 @@ public static class Ids 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; From ce4c92cc989ea1028e29254a9313b1aa67813806 Mon Sep 17 00:00:00 2001 From: SigiRab Date: Fri, 28 Nov 2025 15:31:44 +0100 Subject: [PATCH 04/13] Add files via upload --- .../ClientApi/PlcTagExtend.cs | 710 ++++++++++++++++++ 1 file changed, 710 insertions(+) create mode 100644 src/S7CommPlusDriver/ClientApi/PlcTagExtend.cs diff --git a/src/S7CommPlusDriver/ClientApi/PlcTagExtend.cs b/src/S7CommPlusDriver/ClientApi/PlcTagExtend.cs new file mode 100644 index 0000000..7bae1bf --- /dev/null +++ b/src/S7CommPlusDriver/ClientApi/PlcTagExtend.cs @@ -0,0 +1,710 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace S7CommPlusDriver.ClientApi; + +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); + } +} From 9cfa84b34a8499f7af69a063f205f7f2f95ef48d Mon Sep 17 00:00:00 2001 From: SigiRab Date: Fri, 28 Nov 2025 15:39:09 +0100 Subject: [PATCH 05/13] Update PlcTags.cs extended for array-types --- src/S7CommPlusDriver/ClientApi/PlcTags.cs | 30 ++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/S7CommPlusDriver/ClientApi/PlcTags.cs b/src/S7CommPlusDriver/ClientApi/PlcTags.cs index 368b9d9..6a43eb0 100644 --- a/src/S7CommPlusDriver/ClientApi/PlcTags.cs +++ b/src/S7CommPlusDriver/ClientApi/PlcTags.cs @@ -69,25 +69,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); @@ -98,9 +112,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); @@ -118,6 +136,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: @@ -129,12 +149,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: From b9acd44f2abbfb2c684b0349937a5b5d89328521 Mon Sep 17 00:00:00 2001 From: SigiRab Date: Fri, 28 Nov 2025 15:42:26 +0100 Subject: [PATCH 06/13] Update S7CommPlusConnection.cs --- src/S7CommPlusDriver/S7CommPlusConnection.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/S7CommPlusDriver/S7CommPlusConnection.cs b/src/S7CommPlusDriver/S7CommPlusConnection.cs index 2bcb801..8a16b7e 100644 --- a/src/S7CommPlusDriver/S7CommPlusConnection.cs +++ b/src/S7CommPlusDriver/S7CommPlusConnection.cs @@ -1080,9 +1080,17 @@ private PlcTag browsePlcTagBySymbol(uint ti_relid, ref string symbol, VarInfo va 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()) { - calcAccessSeqFor1DimArray(ref symbol, varType, varInfo); + if (symbol == "") + { + is1Dim = true; + } + else + { + calcAccessSeqFor1DimArray(ref symbol, varType, varInfo); + } } if (varType.OffsetInfoType.IsMDim()) { @@ -1090,6 +1098,10 @@ private PlcTag browsePlcTagBySymbol(uint ti_relid, ref string symbol, VarInfo va } 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; @@ -1102,7 +1114,7 @@ private PlcTag browsePlcTagBySymbol(uint ti_relid, ref string symbol, VarInfo va } else { - return PlcTags.TagFactory(varInfo.Name, new ItemAddress(varInfo.AccessSequence), varType.Softdatatype); + return PlcTags.TagFactory(varInfo.Name, new ItemAddress(varInfo.AccessSequence), varType.Softdatatype, is1Dim); } } From b61c12637e6f0c59681afe29953377af199662cf Mon Sep 17 00:00:00 2001 From: SigiRab Date: Fri, 28 Nov 2025 15:43:12 +0100 Subject: [PATCH 07/13] Update S7CommPlusConnection.cs extended for array-types From b84fb52b4c9d9f66de23b74fa45b2e0ed8e14f98 Mon Sep 17 00:00:00 2001 From: Thomas Wiens Date: Wed, 3 Dec 2025 18:35:44 +0100 Subject: [PATCH 08/13] Move Array-Tag classes into single PlcTags file --- src/S7CommPlusDriver/ClientApi/PlcTag.cs | 709 +++++++++++++++++ .../ClientApi/PlcTagExtend.cs | 710 ------------------ 2 files changed, 709 insertions(+), 710 deletions(-) delete mode 100644 src/S7CommPlusDriver/ClientApi/PlcTagExtend.cs 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/PlcTagExtend.cs b/src/S7CommPlusDriver/ClientApi/PlcTagExtend.cs deleted file mode 100644 index 7bae1bf..0000000 --- a/src/S7CommPlusDriver/ClientApi/PlcTagExtend.cs +++ /dev/null @@ -1,710 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Text; - -namespace S7CommPlusDriver.ClientApi; - -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); - } -} From ef73ef30b2a27d3a45da0a7c63a47e24133b520c Mon Sep 17 00:00:00 2001 From: z Date: Mon, 23 Mar 2026 11:22:01 +0800 Subject: [PATCH 09/13] add function: support for S7-1507S F software controller(min FW:21.9) --- .vs/S7CommPlusDriver/v17/.wsuo | Bin 0 -> 13824 bytes .vs/S7CommPlusDriver/v17/DocumentLayout.json | 97 +++++ .../Legitimation/Legitimation.cs | 55 ++- .../Legitimation/Legitimation.cs.bak | 392 ++++++++++++++++++ 4 files changed, 537 insertions(+), 7 deletions(-) create mode 100644 .vs/S7CommPlusDriver/v17/.wsuo create mode 100644 .vs/S7CommPlusDriver/v17/DocumentLayout.json create mode 100644 src/S7CommPlusDriver/Legitimation/Legitimation.cs.bak diff --git a/.vs/S7CommPlusDriver/v17/.wsuo b/.vs/S7CommPlusDriver/v17/.wsuo new file mode 100644 index 0000000000000000000000000000000000000000..6bfc261eedb41bff1b4e15028361ec8b8400fa38 GIT binary patch literal 13824 zcmeHOO^h5z6|UJ3AUFhK2L}R}B`hJ3)TX_5EpYmLP8wEg)4WY*nD60)J*T}PP=C&BpGRI^t!96 ztLoLO_fxNa=C5}ied`x*KJhOx5Kf5)#NB%ji4PX?YlXP@xDX#fnt6Bc-Mhz6j3as& zrf39KL<_$iF)PlBExg;}23c7-92XDvc>dshYW?WP5C8Fdzx!wH2+JMM3tt4rmUvY> z59}FYXT|IHX=U%{<6@?VpToZQ>%pltWJ7#a(D;dWTWp{QZIJ>R7>f{)cFPTW&54;> zk=?_}74yH2#Ii@#O zkM#8a76lJM>)*Sn|Es`zsQ+sCd$yDIfOdd-`vl@zhd+ck^BAh=KW$N7dhheo$fqsX zn|qjguSbBqJqR@4$xr%uoU^O&pK8rHgL4S&Kj#C=6Z!ucz-Iv`0G|WQ0zMCT67U7U z9N>$9rvP69JPr6V;46S<0M7y>U>@)szyO#43s48x00-a#iZq=ZNS`XCPb0nvI0IM$ zoCTZ%d=qdU@I2rJz_$P|0+sHOJhE_+R9eZf^-VBluuf z@T1?iro6sbTt%v*3zfWQ$M8SDhW6+yF%05gL?2T0;TpWrmhuR9AdB>ocagKdM^@MW z)P7ptYWef4_)MNLETZKWux$co8}F)qZ(f@^JyWY8p>4a)FMF@xBXyhMlH&d~=vEWd z?EwE(^rwwjtUTlW6zBEyllXyUHoNejKxW5m7`s&=ECg#L;KNnb6O4b2{>3QtQ}ng{EyKU2W`Qlt_1ox4J`RKQ!T$Devk!yc4@z^b zuGb%B_>212p95Zk^!51w`LEH>qTf{&Kkc<1e?%t=uiViy$#~7o?VEu*Vis1}z|VxI z$7fj}E$>8l<*om2owr{7>8NdOhMk?X{ld9%>#zLn=*;7fj9-7${tlKu$Zuudq5nU* z|IcxJzX#fZNpxt^`m}?S;HMpD{eBzU3rJQDllH$dqLPA~4+g^|d4p|LNz;e?E#Qne zDb~CG7}tcHYi2R~H{p$O?qA1Rk)APEBppD8nV9yP-uo^}Zy?1r#4hG&dT#4jof`lP zF|IE+@uT-ful5G=R*}2j^DguD8{J$JEBbi=pLMM4*^-CS^h#po9nxD|N3U;BY0oz? zYNVvCsQCSqQqch?$+_u~Aj-+V!=U@vb; zNg+=)=z&p}c<#Y757up|oX!LHQNJU;tCJ(Y+5brbcfazz zyy8ldJG^Z9Jh!omKiNx_$BK0r9~v5B?#SDxpZUoj&cFGOU*9?Qt2Hd-8p6*WyM6ce zi5GwQM)I4Vo%l~Mw7NP6;2L@^4m{IM>XG!)dM53_w56ZAzVwnTHC@+r5-(Cj)w)a5 zg{uoBeNi?poiy4c_4OO~`9FOq>_doqb4&x}JWP7>?p;&Ui&)-6^rfzYx7 zOQv-%k+vBpGBi9-rcoG&u^szyY-?F0*3mi80=9is9_yplgMjV)?VL0G&p>@}ef|yd|KClVM;w()0o=G#oRt+^A4Ibzn2sdd!CkS+N~yZV@51G5ca_GJyB*(YQQ}+tGi|#LocbEUTny~LM*c!&xqX2cTInP zFY>;WKM&7;56^$Muvf?P=EL*f^7H5a$NBFvyrIi#&vfs3cX7|POoN{q+4wLSe0B`}bIrgzAPj@_ z=Qz3ZlK&InG$e5wmXSMrJhNbKF_8bHTZiJeVR7EA)$17rt wFGC8rqT=1SPVWvxzh!?6{V$GxzoqeFhv)y3o&QsBdG43n_y7O^ literal 0 HcmV?d00001 diff --git a/.vs/S7CommPlusDriver/v17/DocumentLayout.json b/.vs/S7CommPlusDriver/v17/DocumentLayout.json new file mode 100644 index 0000000..3b2eef4 --- /dev/null +++ b/.vs/S7CommPlusDriver/v17/DocumentLayout.json @@ -0,0 +1,97 @@ +{ + "Version": 1, + "WorkspaceRootPath": "C:\\Users\\zhangf7.ZF-WORLD\\source\\repos\\S7CommPlusDriver\\", + "Documents": [], + "DocumentGroupContainers": [ + { + "Orientation": 0, + "VerticalTabListWidth": 256, + "DocumentGroups": [ + { + "DockedWidth": 194, + "SelectedChildIndex": -1, + "Children": [ + { + "$type": "Bookmark", + "Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}" + }, + { + "$type": "Bookmark", + "Name": "ST:132:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:128:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:135:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:132:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:133:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:134:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:130:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:137:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:142:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:143:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{1c4feeaa-4718-4aa9-859d-94ce25d182ba}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{3ae79031-e1bc-11d0-8f78-00a0c9110057}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{1c64b9c2-e352-428e-a56d-0ace190b99a6}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{de1fc918-f32e-4dd7-a915-1792a051f26b}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{e5c86464-96be-4d7c-9a8b-abcb3bbf5f92}" + } + ] + }, + { + "DockedWidth": 206, + "SelectedChildIndex": -1, + "Children": [ + { + "$type": "Bookmark", + "Name": "ST:928770227:0:{81164725-9a96-4ece-a4cb-440d8fd285e5}" + }, + { + "$type": "Bookmark", + "Name": "ST:254354193:0:{71f361cc-493f-47c0-923f-f2570b6f8618}" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/S7CommPlusDriver/Legitimation/Legitimation.cs b/src/S7CommPlusDriver/Legitimation/Legitimation.cs index 8050fd5..7901ced 100644 --- a/src/S7CommPlusDriver/Legitimation/Legitimation.cs +++ b/src/S7CommPlusDriver/Legitimation/Legitimation.cs @@ -23,23 +23,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) { @@ -55,7 +87,7 @@ private int legitimate(ValueStruct serverSession, string password, string userna { legacyLegitimation = false; } - else if (deviceVersion.StartsWith("2")) + else if (deviceVersion.StartsWith("2")) // S7-1200 (2xx) { if (fwVerNo < 403) { @@ -67,6 +99,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/Legitimation/Legitimation.cs.bak b/src/S7CommPlusDriver/Legitimation/Legitimation.cs.bak new file mode 100644 index 0000000..d04e921 --- /dev/null +++ b/src/S7CommPlusDriver/Legitimation/Legitimation.cs.bak @@ -0,0 +1,392 @@ +using S7CommPlusDriver.Legitimation; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace S7CommPlusDriver { + public partial class S7CommPlusConnection + { + + private byte[] omsSecret; + + /// + /// Legitimation stage of the connect routine + /// + /// Server sesstion information containing the firmware version + /// PLC password + /// PLC username (leave empty for legacy login) + /// 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(); + 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; + + // 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 (fwVerNo < 209) + { + Console.WriteLine("S7CommPlusConnection - Legitimate: Firmware version is not supported!"); + return S7Consts.errCliFirmwareNotSupported; + } + if (fwVerNo < 301) + { + legacyLegitimation = true; + } + } + 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")) + { + if (fwVerNo < 403) + { + Console.WriteLine("S7CommPlusConnection - Legitimate: Firmware version is not supported!"); + return S7Consts.errCliFirmwareNotSupported; + } + if (fwVerNo < 407) + { + 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!"); + return S7Consts.errCliDeviceNotSupported; + } + + // Get current protection level + var getVarSubstreamedReq = new GetVarSubstreamedRequest(ProtocolVersion.V2); + getVarSubstreamedReq.InObjectId = m_SessionId; + getVarSubstreamedReq.SessionId = m_SessionId; + getVarSubstreamedReq.Address = Ids.EffectiveProtectionLevel; + int res = SendS7plusFunctionObject(getVarSubstreamedReq); + 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 getVarSubstreamedRes = GetVarSubstreamedResponse.DeserializeFromPdu(m_ReceivedPDU); + if (getVarSubstreamedRes == null) + { + Console.WriteLine("S7CommPlusConnection - Legitimate: GetVarSubstreamedResponse with Error!"); + m_client.Disconnect(); + return S7Consts.errIsoInvalidPDU; + } + + // Check access level + UInt32 accessLevel = (getVarSubstreamedRes.Value as ValueUDInt).GetValue(); + if (accessLevel > AccessLevel.FullAccess && password != "") + { + // Legitimate + if (legacyLegitimation) + { + return legitimateLegacy(password); + } + else + { + return legitimateNew(password, username); + } + + } + else if (accessLevel > AccessLevel.FullAccess) + { + Console.WriteLine("S7CommPlusConnection - Legitimate: Warning: Access level is not fullaccess but no password set!"); + } + + return 0; + } + + /// + /// Legitimate using the new login method (firmware >= 3.1) + /// + /// PLC password + /// PLC username (leave empy for legacy login) + /// error code (0 = ok) + private int legitimateNew(string password, string username = "") + { + // Get challenge + var getVarSubstreamedReq_challange = new GetVarSubstreamedRequest(ProtocolVersion.V2); + getVarSubstreamedReq_challange.InObjectId = m_SessionId; + getVarSubstreamedReq_challange.SessionId = m_SessionId; + getVarSubstreamedReq_challange.Address = Ids.ServerSessionRequest; + int res = SendS7plusFunctionObject(getVarSubstreamedReq_challange); + 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 getVarSubstreamedRes_challenge = GetVarSubstreamedResponse.DeserializeFromPdu(m_ReceivedPDU); + if (getVarSubstreamedRes_challenge == null) + { + Console.WriteLine("S7CommPlusConnection - Legitimate: getVarSubstreamedRes_challenge with Error!"); + m_client.Disconnect(); + return S7Consts.errIsoInvalidPDU; + } + + byte[] challenge = (getVarSubstreamedRes_challenge.Value as ValueUSIntArray).GetValue(); + + // Encrypt challengeResponse + byte[] challengeResponse; + if (omsSecret == null || omsSecret.Length != 32) + { + // Create oms exporter secret + omsSecret = m_client.getOMSExporterSecret(); + } + // Roll key + byte[] key = LegitimationCrypto.sha256(omsSecret); + omsSecret = key; + + // Use the first 16 bytes of the challenge as iv + byte[] iv = new ArraySegment(challenge, 0, 16).ToArray(); + // Encrypt + challengeResponse = LegitimationCrypto.EncryptAesCbc(buildLegitimationPayload(password, username), key, iv); + + // Send challengeResponse + var setVariableReq = new SetVariableRequest(ProtocolVersion.V2); + setVariableReq.InObjectId = m_SessionId; + setVariableReq.SessionId = m_SessionId; + setVariableReq.Address = Ids.Legitimate; + setVariableReq.Value = new ValueBlob(0, challengeResponse); + res = SendS7plusFunctionObject(setVariableReq); + 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 setVariableResponse = SetVariableResponse.DeserializeFromPdu(m_ReceivedPDU); + if (setVariableResponse == null) + { + Console.WriteLine("S7CommPlusConnection - Legitimate: setVariableResponse with Error!"); + m_client.Disconnect(); + return S7Consts.errIsoInvalidPDU; + } + // Check if the legitimation attempt was successful + Int16 errorCode = (Int16)setVariableResponse.ReturnValue; + if (errorCode < 0) + { + Console.WriteLine("S7CommPlusConnection - Legitimate: access denied"); + m_client.Disconnect(); + return S7Consts.errCliAccessDenied; + } + + return 0; + } + + /// + /// Builds the legitimation payload from given username and password. + /// If username is empty the payload for a legacy login will be build. + /// If username is not empty the payload for the new login is build. + /// + /// PLC password + /// PLC username (optional) + /// Build payload + private static byte[] buildLegitimationPayload(string password, string username = "") + { + ValueStruct payload = new ValueStruct(Ids.LID_LegitimationPayloadStruct); + if (username != "") + { + // Login with username and password = new login + payload.AddStructElement(Ids.LID_LegitimationPayloadType, new ValueUDInt(LegitimationType.New)); + payload.AddStructElement(Ids.LID_LegitimationPayloadUsername, new ValueBlob(0, Encoding.UTF8.GetBytes(username))); + payload.AddStructElement(Ids.LID_LegitimationPayloadPassword, new ValueBlob(0, Encoding.UTF8.GetBytes(password))); + + } + else + { + // Login with only password = legacy login + // Hash password + byte[] hashedPw; + using (SHA1Managed sha1 = new SHA1Managed()) + { + hashedPw = sha1.ComputeHash(Encoding.UTF8.GetBytes(password)); + } + + payload.AddStructElement(Ids.LID_LegitimationPayloadType, new ValueUDInt(LegitimationType.Legacy)); + payload.AddStructElement(Ids.LID_LegitimationPayloadUsername, new ValueBlob(0, Encoding.UTF8.GetBytes(username))); + payload.AddStructElement(Ids.LID_LegitimationPayloadPassword, new ValueBlob(0, hashedPw)); + } + using (var memStr = new MemoryStream()) + { + payload.Serialize(memStr); + return memStr.ToArray(); + } + } + + /// + /// Legitimate using the old legacy login (firmware version < 3.1) + /// + /// PLC password + /// error code (0 = OK) + private int legitimateLegacy(string password) + { + + // Get challenge + var getVarSubstreamedReq_challange = new GetVarSubstreamedRequest(ProtocolVersion.V2); + getVarSubstreamedReq_challange.InObjectId = m_SessionId; + getVarSubstreamedReq_challange.SessionId = m_SessionId; + getVarSubstreamedReq_challange.Address = Ids.ServerSessionRequest; + int res = SendS7plusFunctionObject(getVarSubstreamedReq_challange); + 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 getVarSubstreamedRes_challenge = GetVarSubstreamedResponse.DeserializeFromPdu(m_ReceivedPDU); + if (getVarSubstreamedRes_challenge == null) + { + Console.WriteLine("S7CommPlusConnection - Legitimate: getVarSubstreamedRes_challenge with Error!"); + m_client.Disconnect(); + return S7Consts.errIsoInvalidPDU; + } + + byte[] challenge = (getVarSubstreamedRes_challenge.Value as ValueUSIntArray).GetValue(); + + // Calculate challengeResponse [sha1(password) xor challenge] + byte[] challengeResponse; + using (SHA1Managed sha1 = new SHA1Managed()) + { + challengeResponse = sha1.ComputeHash(Encoding.UTF8.GetBytes(password)); + } + if (challengeResponse.Length != challenge.Length) + { + Console.WriteLine("S7CommPlusConnection - Legitimate: challengeResponse.Length != challenge.Length"); + m_client.Disconnect(); + return S7Consts.errIsoInvalidPDU; + } + for (int i = 0; i < challengeResponse.Length; ++i) + { + challengeResponse[i] = (byte)(challengeResponse[i] ^ challenge[i]); + } + + // Send challengeResponse + var setVariableReq = new SetVariableRequest(ProtocolVersion.V2); + setVariableReq.InObjectId = m_SessionId; + setVariableReq.SessionId = m_SessionId; + setVariableReq.Address = Ids.ServerSessionResponse; + setVariableReq.Value = new ValueUSIntArray(challengeResponse); + res = SendS7plusFunctionObject(setVariableReq); + 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 setVariableResponse = SetVariableResponse.DeserializeFromPdu(m_ReceivedPDU); + if (setVariableResponse == null) + { + Console.WriteLine("S7CommPlusConnection - Legitimate: setVariableResponse with Error!"); + m_client.Disconnect(); + return S7Consts.errIsoInvalidPDU; + } + // Check if the legitimation attempt was successful + Int16 errorCode = (Int16)setVariableResponse.ReturnValue; + if (errorCode < 0) + { + Console.WriteLine("S7CommPlusConnection - Legitimate: access denied"); + m_client.Disconnect(); + return S7Consts.errCliAccessDenied; + } + + return 0; + } + } +} From 1a91c494e24ce6fc9e899623df4b7d04cd0f3484 Mon Sep 17 00:00:00 2001 From: z Date: Mon, 23 Mar 2026 12:03:08 +0800 Subject: [PATCH 10/13] Revert "add function: support for S7-1507S F software controller(min FW:21.9)" This reverts commit ef73ef30b2a27d3a45da0a7c63a47e24133b520c. --- .vs/S7CommPlusDriver/v17/.wsuo | Bin 13824 -> 0 bytes .vs/S7CommPlusDriver/v17/DocumentLayout.json | 97 ----- .../Legitimation/Legitimation.cs | 55 +-- .../Legitimation/Legitimation.cs.bak | 392 ------------------ 4 files changed, 7 insertions(+), 537 deletions(-) delete mode 100644 .vs/S7CommPlusDriver/v17/.wsuo delete mode 100644 .vs/S7CommPlusDriver/v17/DocumentLayout.json delete mode 100644 src/S7CommPlusDriver/Legitimation/Legitimation.cs.bak diff --git a/.vs/S7CommPlusDriver/v17/.wsuo b/.vs/S7CommPlusDriver/v17/.wsuo deleted file mode 100644 index 6bfc261eedb41bff1b4e15028361ec8b8400fa38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13824 zcmeHOO^h5z6|UJ3AUFhK2L}R}B`hJ3)TX_5EpYmLP8wEg)4WY*nD60)J*T}PP=C&BpGRI^t!96 ztLoLO_fxNa=C5}ied`x*KJhOx5Kf5)#NB%ji4PX?YlXP@xDX#fnt6Bc-Mhz6j3as& zrf39KL<_$iF)PlBExg;}23c7-92XDvc>dshYW?WP5C8Fdzx!wH2+JMM3tt4rmUvY> z59}FYXT|IHX=U%{<6@?VpToZQ>%pltWJ7#a(D;dWTWp{QZIJ>R7>f{)cFPTW&54;> zk=?_}74yH2#Ii@#O zkM#8a76lJM>)*Sn|Es`zsQ+sCd$yDIfOdd-`vl@zhd+ck^BAh=KW$N7dhheo$fqsX zn|qjguSbBqJqR@4$xr%uoU^O&pK8rHgL4S&Kj#C=6Z!ucz-Iv`0G|WQ0zMCT67U7U z9N>$9rvP69JPr6V;46S<0M7y>U>@)szyO#43s48x00-a#iZq=ZNS`XCPb0nvI0IM$ zoCTZ%d=qdU@I2rJz_$P|0+sHOJhE_+R9eZf^-VBluuf z@T1?iro6sbTt%v*3zfWQ$M8SDhW6+yF%05gL?2T0;TpWrmhuR9AdB>ocagKdM^@MW z)P7ptYWef4_)MNLETZKWux$co8}F)qZ(f@^JyWY8p>4a)FMF@xBXyhMlH&d~=vEWd z?EwE(^rwwjtUTlW6zBEyllXyUHoNejKxW5m7`s&=ECg#L;KNnb6O4b2{>3QtQ}ng{EyKU2W`Qlt_1ox4J`RKQ!T$Devk!yc4@z^b zuGb%B_>212p95Zk^!51w`LEH>qTf{&Kkc<1e?%t=uiViy$#~7o?VEu*Vis1}z|VxI z$7fj}E$>8l<*om2owr{7>8NdOhMk?X{ld9%>#zLn=*;7fj9-7${tlKu$Zuudq5nU* z|IcxJzX#fZNpxt^`m}?S;HMpD{eBzU3rJQDllH$dqLPA~4+g^|d4p|LNz;e?E#Qne zDb~CG7}tcHYi2R~H{p$O?qA1Rk)APEBppD8nV9yP-uo^}Zy?1r#4hG&dT#4jof`lP zF|IE+@uT-ful5G=R*}2j^DguD8{J$JEBbi=pLMM4*^-CS^h#po9nxD|N3U;BY0oz? zYNVvCsQCSqQqch?$+_u~Aj-+V!=U@vb; zNg+=)=z&p}c<#Y757up|oX!LHQNJU;tCJ(Y+5brbcfazz zyy8ldJG^Z9Jh!omKiNx_$BK0r9~v5B?#SDxpZUoj&cFGOU*9?Qt2Hd-8p6*WyM6ce zi5GwQM)I4Vo%l~Mw7NP6;2L@^4m{IM>XG!)dM53_w56ZAzVwnTHC@+r5-(Cj)w)a5 zg{uoBeNi?poiy4c_4OO~`9FOq>_doqb4&x}JWP7>?p;&Ui&)-6^rfzYx7 zOQv-%k+vBpGBi9-rcoG&u^szyY-?F0*3mi80=9is9_yplgMjV)?VL0G&p>@}ef|yd|KClVM;w()0o=G#oRt+^A4Ibzn2sdd!CkS+N~yZV@51G5ca_GJyB*(YQQ}+tGi|#LocbEUTny~LM*c!&xqX2cTInP zFY>;WKM&7;56^$Muvf?P=EL*f^7H5a$NBFvyrIi#&vfs3cX7|POoN{q+4wLSe0B`}bIrgzAPj@_ z=Qz3ZlK&InG$e5wmXSMrJhNbKF_8bHTZiJeVR7EA)$17rt wFGC8rqT=1SPVWvxzh!?6{V$GxzoqeFhv)y3o&QsBdG43n_y7O^ diff --git a/.vs/S7CommPlusDriver/v17/DocumentLayout.json b/.vs/S7CommPlusDriver/v17/DocumentLayout.json deleted file mode 100644 index 3b2eef4..0000000 --- a/.vs/S7CommPlusDriver/v17/DocumentLayout.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "Version": 1, - "WorkspaceRootPath": "C:\\Users\\zhangf7.ZF-WORLD\\source\\repos\\S7CommPlusDriver\\", - "Documents": [], - "DocumentGroupContainers": [ - { - "Orientation": 0, - "VerticalTabListWidth": 256, - "DocumentGroups": [ - { - "DockedWidth": 194, - "SelectedChildIndex": -1, - "Children": [ - { - "$type": "Bookmark", - "Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}" - }, - { - "$type": "Bookmark", - "Name": "ST:132:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" - }, - { - "$type": "Bookmark", - "Name": "ST:128:0:{1fc202d4-d401-403c-9834-5b218574bb67}" - }, - { - "$type": "Bookmark", - "Name": "ST:135:0:{1fc202d4-d401-403c-9834-5b218574bb67}" - }, - { - "$type": "Bookmark", - "Name": "ST:132:0:{1fc202d4-d401-403c-9834-5b218574bb67}" - }, - { - "$type": "Bookmark", - "Name": "ST:133:0:{1fc202d4-d401-403c-9834-5b218574bb67}" - }, - { - "$type": "Bookmark", - "Name": "ST:134:0:{1fc202d4-d401-403c-9834-5b218574bb67}" - }, - { - "$type": "Bookmark", - "Name": "ST:130:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" - }, - { - "$type": "Bookmark", - "Name": "ST:137:0:{1fc202d4-d401-403c-9834-5b218574bb67}" - }, - { - "$type": "Bookmark", - "Name": "ST:142:0:{1fc202d4-d401-403c-9834-5b218574bb67}" - }, - { - "$type": "Bookmark", - "Name": "ST:143:0:{1fc202d4-d401-403c-9834-5b218574bb67}" - }, - { - "$type": "Bookmark", - "Name": "ST:0:0:{1c4feeaa-4718-4aa9-859d-94ce25d182ba}" - }, - { - "$type": "Bookmark", - "Name": "ST:0:0:{3ae79031-e1bc-11d0-8f78-00a0c9110057}" - }, - { - "$type": "Bookmark", - "Name": "ST:0:0:{1c64b9c2-e352-428e-a56d-0ace190b99a6}" - }, - { - "$type": "Bookmark", - "Name": "ST:0:0:{de1fc918-f32e-4dd7-a915-1792a051f26b}" - }, - { - "$type": "Bookmark", - "Name": "ST:0:0:{e5c86464-96be-4d7c-9a8b-abcb3bbf5f92}" - } - ] - }, - { - "DockedWidth": 206, - "SelectedChildIndex": -1, - "Children": [ - { - "$type": "Bookmark", - "Name": "ST:928770227:0:{81164725-9a96-4ece-a4cb-440d8fd285e5}" - }, - { - "$type": "Bookmark", - "Name": "ST:254354193:0:{71f361cc-493f-47c0-923f-f2570b6f8618}" - } - ] - } - ] - } - ] -} \ No newline at end of file diff --git a/src/S7CommPlusDriver/Legitimation/Legitimation.cs b/src/S7CommPlusDriver/Legitimation/Legitimation.cs index 7901ced..8050fd5 100644 --- a/src/S7CommPlusDriver/Legitimation/Legitimation.cs +++ b/src/S7CommPlusDriver/Legitimation/Legitimation.cs @@ -23,55 +23,23 @@ 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(); - var reVersions = new Regex( - @"^[^;]*;[^;]*[17]\s?(\d{3}).*;[VS](\d{1,2}\.\d+)$", - RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase - ); + Regex reVersions = new Regex("^.*;.*[17]\\s?([52]\\d\\d).+;[VS](\\d\\.\\d)$"); 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; // 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; - } + 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]); // Check if we have to use legacy legitimation via the firmware version bool legacyLegitimation = false; - if (deviceVersion.StartsWith("5")) // S7-1500 (5xx) + if (deviceVersion.StartsWith("5")) { if (fwVerNo < 209) { @@ -87,7 +55,7 @@ private int legitimate(ValueStruct serverSession, string password, string userna { legacyLegitimation = false; } - else if (deviceVersion.StartsWith("2")) // S7-1200 (2xx) + else if (deviceVersion.StartsWith("2")) { if (fwVerNo < 403) { @@ -99,15 +67,6 @@ 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/Legitimation/Legitimation.cs.bak b/src/S7CommPlusDriver/Legitimation/Legitimation.cs.bak deleted file mode 100644 index d04e921..0000000 --- a/src/S7CommPlusDriver/Legitimation/Legitimation.cs.bak +++ /dev/null @@ -1,392 +0,0 @@ -using S7CommPlusDriver.Legitimation; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; - -namespace S7CommPlusDriver { - public partial class S7CommPlusConnection - { - - private byte[] omsSecret; - - /// - /// Legitimation stage of the connect routine - /// - /// Server sesstion information containing the firmware version - /// PLC password - /// PLC username (leave empty for legacy login) - /// 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(); - 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; - - // 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 (fwVerNo < 209) - { - Console.WriteLine("S7CommPlusConnection - Legitimate: Firmware version is not supported!"); - return S7Consts.errCliFirmwareNotSupported; - } - if (fwVerNo < 301) - { - legacyLegitimation = true; - } - } - 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")) - { - if (fwVerNo < 403) - { - Console.WriteLine("S7CommPlusConnection - Legitimate: Firmware version is not supported!"); - return S7Consts.errCliFirmwareNotSupported; - } - if (fwVerNo < 407) - { - 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!"); - return S7Consts.errCliDeviceNotSupported; - } - - // Get current protection level - var getVarSubstreamedReq = new GetVarSubstreamedRequest(ProtocolVersion.V2); - getVarSubstreamedReq.InObjectId = m_SessionId; - getVarSubstreamedReq.SessionId = m_SessionId; - getVarSubstreamedReq.Address = Ids.EffectiveProtectionLevel; - int res = SendS7plusFunctionObject(getVarSubstreamedReq); - 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 getVarSubstreamedRes = GetVarSubstreamedResponse.DeserializeFromPdu(m_ReceivedPDU); - if (getVarSubstreamedRes == null) - { - Console.WriteLine("S7CommPlusConnection - Legitimate: GetVarSubstreamedResponse with Error!"); - m_client.Disconnect(); - return S7Consts.errIsoInvalidPDU; - } - - // Check access level - UInt32 accessLevel = (getVarSubstreamedRes.Value as ValueUDInt).GetValue(); - if (accessLevel > AccessLevel.FullAccess && password != "") - { - // Legitimate - if (legacyLegitimation) - { - return legitimateLegacy(password); - } - else - { - return legitimateNew(password, username); - } - - } - else if (accessLevel > AccessLevel.FullAccess) - { - Console.WriteLine("S7CommPlusConnection - Legitimate: Warning: Access level is not fullaccess but no password set!"); - } - - return 0; - } - - /// - /// Legitimate using the new login method (firmware >= 3.1) - /// - /// PLC password - /// PLC username (leave empy for legacy login) - /// error code (0 = ok) - private int legitimateNew(string password, string username = "") - { - // Get challenge - var getVarSubstreamedReq_challange = new GetVarSubstreamedRequest(ProtocolVersion.V2); - getVarSubstreamedReq_challange.InObjectId = m_SessionId; - getVarSubstreamedReq_challange.SessionId = m_SessionId; - getVarSubstreamedReq_challange.Address = Ids.ServerSessionRequest; - int res = SendS7plusFunctionObject(getVarSubstreamedReq_challange); - 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 getVarSubstreamedRes_challenge = GetVarSubstreamedResponse.DeserializeFromPdu(m_ReceivedPDU); - if (getVarSubstreamedRes_challenge == null) - { - Console.WriteLine("S7CommPlusConnection - Legitimate: getVarSubstreamedRes_challenge with Error!"); - m_client.Disconnect(); - return S7Consts.errIsoInvalidPDU; - } - - byte[] challenge = (getVarSubstreamedRes_challenge.Value as ValueUSIntArray).GetValue(); - - // Encrypt challengeResponse - byte[] challengeResponse; - if (omsSecret == null || omsSecret.Length != 32) - { - // Create oms exporter secret - omsSecret = m_client.getOMSExporterSecret(); - } - // Roll key - byte[] key = LegitimationCrypto.sha256(omsSecret); - omsSecret = key; - - // Use the first 16 bytes of the challenge as iv - byte[] iv = new ArraySegment(challenge, 0, 16).ToArray(); - // Encrypt - challengeResponse = LegitimationCrypto.EncryptAesCbc(buildLegitimationPayload(password, username), key, iv); - - // Send challengeResponse - var setVariableReq = new SetVariableRequest(ProtocolVersion.V2); - setVariableReq.InObjectId = m_SessionId; - setVariableReq.SessionId = m_SessionId; - setVariableReq.Address = Ids.Legitimate; - setVariableReq.Value = new ValueBlob(0, challengeResponse); - res = SendS7plusFunctionObject(setVariableReq); - 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 setVariableResponse = SetVariableResponse.DeserializeFromPdu(m_ReceivedPDU); - if (setVariableResponse == null) - { - Console.WriteLine("S7CommPlusConnection - Legitimate: setVariableResponse with Error!"); - m_client.Disconnect(); - return S7Consts.errIsoInvalidPDU; - } - // Check if the legitimation attempt was successful - Int16 errorCode = (Int16)setVariableResponse.ReturnValue; - if (errorCode < 0) - { - Console.WriteLine("S7CommPlusConnection - Legitimate: access denied"); - m_client.Disconnect(); - return S7Consts.errCliAccessDenied; - } - - return 0; - } - - /// - /// Builds the legitimation payload from given username and password. - /// If username is empty the payload for a legacy login will be build. - /// If username is not empty the payload for the new login is build. - /// - /// PLC password - /// PLC username (optional) - /// Build payload - private static byte[] buildLegitimationPayload(string password, string username = "") - { - ValueStruct payload = new ValueStruct(Ids.LID_LegitimationPayloadStruct); - if (username != "") - { - // Login with username and password = new login - payload.AddStructElement(Ids.LID_LegitimationPayloadType, new ValueUDInt(LegitimationType.New)); - payload.AddStructElement(Ids.LID_LegitimationPayloadUsername, new ValueBlob(0, Encoding.UTF8.GetBytes(username))); - payload.AddStructElement(Ids.LID_LegitimationPayloadPassword, new ValueBlob(0, Encoding.UTF8.GetBytes(password))); - - } - else - { - // Login with only password = legacy login - // Hash password - byte[] hashedPw; - using (SHA1Managed sha1 = new SHA1Managed()) - { - hashedPw = sha1.ComputeHash(Encoding.UTF8.GetBytes(password)); - } - - payload.AddStructElement(Ids.LID_LegitimationPayloadType, new ValueUDInt(LegitimationType.Legacy)); - payload.AddStructElement(Ids.LID_LegitimationPayloadUsername, new ValueBlob(0, Encoding.UTF8.GetBytes(username))); - payload.AddStructElement(Ids.LID_LegitimationPayloadPassword, new ValueBlob(0, hashedPw)); - } - using (var memStr = new MemoryStream()) - { - payload.Serialize(memStr); - return memStr.ToArray(); - } - } - - /// - /// Legitimate using the old legacy login (firmware version < 3.1) - /// - /// PLC password - /// error code (0 = OK) - private int legitimateLegacy(string password) - { - - // Get challenge - var getVarSubstreamedReq_challange = new GetVarSubstreamedRequest(ProtocolVersion.V2); - getVarSubstreamedReq_challange.InObjectId = m_SessionId; - getVarSubstreamedReq_challange.SessionId = m_SessionId; - getVarSubstreamedReq_challange.Address = Ids.ServerSessionRequest; - int res = SendS7plusFunctionObject(getVarSubstreamedReq_challange); - 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 getVarSubstreamedRes_challenge = GetVarSubstreamedResponse.DeserializeFromPdu(m_ReceivedPDU); - if (getVarSubstreamedRes_challenge == null) - { - Console.WriteLine("S7CommPlusConnection - Legitimate: getVarSubstreamedRes_challenge with Error!"); - m_client.Disconnect(); - return S7Consts.errIsoInvalidPDU; - } - - byte[] challenge = (getVarSubstreamedRes_challenge.Value as ValueUSIntArray).GetValue(); - - // Calculate challengeResponse [sha1(password) xor challenge] - byte[] challengeResponse; - using (SHA1Managed sha1 = new SHA1Managed()) - { - challengeResponse = sha1.ComputeHash(Encoding.UTF8.GetBytes(password)); - } - if (challengeResponse.Length != challenge.Length) - { - Console.WriteLine("S7CommPlusConnection - Legitimate: challengeResponse.Length != challenge.Length"); - m_client.Disconnect(); - return S7Consts.errIsoInvalidPDU; - } - for (int i = 0; i < challengeResponse.Length; ++i) - { - challengeResponse[i] = (byte)(challengeResponse[i] ^ challenge[i]); - } - - // Send challengeResponse - var setVariableReq = new SetVariableRequest(ProtocolVersion.V2); - setVariableReq.InObjectId = m_SessionId; - setVariableReq.SessionId = m_SessionId; - setVariableReq.Address = Ids.ServerSessionResponse; - setVariableReq.Value = new ValueUSIntArray(challengeResponse); - res = SendS7plusFunctionObject(setVariableReq); - 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 setVariableResponse = SetVariableResponse.DeserializeFromPdu(m_ReceivedPDU); - if (setVariableResponse == null) - { - Console.WriteLine("S7CommPlusConnection - Legitimate: setVariableResponse with Error!"); - m_client.Disconnect(); - return S7Consts.errIsoInvalidPDU; - } - // Check if the legitimation attempt was successful - Int16 errorCode = (Int16)setVariableResponse.ReturnValue; - if (errorCode < 0) - { - Console.WriteLine("S7CommPlusConnection - Legitimate: access denied"); - m_client.Disconnect(); - return S7Consts.errCliAccessDenied; - } - - return 0; - } - } -} From c4eb09925ff6e20c7269b913484a79c39b9a8751 Mon Sep 17 00:00:00 2001 From: z Date: Mon, 23 Mar 2026 12:10:57 +0800 Subject: [PATCH 11/13] support for Software controllers as of firmware version V21.9. --- .../Legitimation/Legitimation.cs | 60 ++++++++++++++++--- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/src/S7CommPlusDriver/Legitimation/Legitimation.cs b/src/S7CommPlusDriver/Legitimation/Legitimation.cs index 8050fd5..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) { @@ -55,7 +88,7 @@ private int legitimate(ValueStruct serverSession, string password, string userna { legacyLegitimation = false; } - else if (deviceVersion.StartsWith("2")) + else if (deviceVersion.StartsWith("2")) // S7-1200 (2xx) { if (fwVerNo < 403) { @@ -67,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!"); From b7e605770c923270d53801d9b841c42ce80c484f Mon Sep 17 00:00:00 2001 From: Michal Zukiewicz Date: Fri, 22 May 2026 14:12:20 +0200 Subject: [PATCH 12/13] White space and line endings fixes --- .../Alarming/AlarmsHandler.cs | 350 +- src/S7CommPlusDriver/Alarming/BrowseAlarms.cs | 6 +- src/S7CommPlusDriver/Net/S7Client.cs | 1420 ++++---- src/S7CommPlusDriver/S7CommPlusConnection.cs | 2870 +++++++++-------- 4 files changed, 2324 insertions(+), 2322 deletions(-) diff --git a/src/S7CommPlusDriver/Alarming/AlarmsHandler.cs b/src/S7CommPlusDriver/Alarming/AlarmsHandler.cs index 61f4b92..f79c4d3 100644 --- a/src/S7CommPlusDriver/Alarming/AlarmsHandler.cs +++ b/src/S7CommPlusDriver/Alarming/AlarmsHandler.cs @@ -1,166 +1,166 @@ -#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 S7CommPlusDriver.Alarming; -using System; -using System.Collections.Generic; - -namespace S7CommPlusDriver -{ - public partial class S7CommPlusConnection - { - // ************************************************* - // * IMPORTANT! * - // * This is basically a test for Alarming, * - // * how to use it, and how to later integrate * - // * this into the complete library! * - // ************************************************* - // - // Example code for testing: - // CultureInfo ci = new CultureInfo("en-US"); - // conn.AlarmSubscriptionCreate(); - // conn.TestWaitForAlarmNotifications(20000, 3, ci.LCID); - // conn.AlarmSubscriptionDelete(); - - uint m_AlarmSubscriptionRelationId = 0x7fffc001; // TODO! Unknown value! See also Subscription.cs - uint m_AlarmSubscriptionRefRelationId = 0x51010001; // TODO! Unknown value! - short m_AlarmNextCreditLimit; - uint m_AlarmSubscriptionObjectId; - - public int AlarmSubscriptionCreate(uint[] languageIds) - { - int res; - PObject subsobj = new PObject(); - subsobj.ClassId = Ids.ClassSubscription; - subsobj.RelationId = m_AlarmSubscriptionRelationId; - subsobj.AddAttribute(Ids.ObjectVariableTypeName, new ValueWString("Subscription_" + m_AlarmSubscriptionRelationId.ToString())); - subsobj.AddAttribute(Ids.SubscriptionFunctionClassId, new ValueUSInt(2)); - subsobj.AddAttribute(Ids.SubscriptionMissedSendings, new ValueUInt(0)); - subsobj.AddAttribute(Ids.SubscriptionSubsystemError, new ValueLInt(0)); - subsobj.AddAttribute(Ids.SubscriptionRouteMode, new ValueUSInt(2)); // TODO Unknown - subsobj.AddAttribute(Ids.SubscriptionActive, new ValueBool(true)); - subsobj.AddAttribute(Ids.SubscriptionReferenceList, new ValueUDIntArray(new uint[3] { 0x80010000, 0, 0 }, 0x20)); // 0x20 = Adressarray - subsobj.AddAttribute(Ids.SubscriptionCycleTime, new ValueUDInt(0)); - subsobj.AddAttribute(Ids.SubscriptionDelayTime, new ValueUDInt(0)); - subsobj.AddAttribute(Ids.SubscriptionDisabled, new ValueUSInt(0)); - subsobj.AddAttribute(Ids.SubscriptionCount, new ValueUSInt(0)); - m_AlarmNextCreditLimit = 10; - subsobj.AddAttribute(Ids.SubscriptionCreditLimit, new ValueInt(m_AlarmNextCreditLimit)); // -1=unlimited, 255 = max - subsobj.AddAttribute(Ids.SubscriptionTicks, new ValueUInt(65535)); // 65535 - // 1055 = Unknown -> is working without setting this. Maybe default attribute is zero. - //subsobj.AddAttribute(1055, new ValueUSInt(0)); - PObject asrefsobj = new PObject(); - asrefsobj.ClassId = Ids.AlarmSubscriptionRef_Class_Rid; - asrefsobj.RelationId = m_AlarmSubscriptionRefRelationId; - asrefsobj.AddAttribute(Ids.ObjectVariableTypeName, new ValueWString("S7pDriver_Alarming")); - asrefsobj.AddAttribute(Ids.SubscriptionReferenceTriggerAndTransmitMode, new ValueUSInt(3)); - asrefsobj.AddAttribute(Ids.AlarmSubscriptionRef_AlarmDomain, new ValueUIntArray(new ushort[10] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 0x10)); - // Also variant to set explicit the alarm domain as filter, for example: - // {1, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272} - // Possibly 65535 is "all". - asrefsobj.AddAttribute(Ids.AlarmSubscriptionRef_AlarmDomain2, new ValueUIntArray(new ushort[1] { 65535 }, 0x20)); // 0x20 = Adressarray - // OPTION: - // Send text informations with the message, we don't need to browse them in advance. - asrefsobj.AddAttribute(Ids.AlarmSubscriptionRef_AlarmTextLanguages_Rid, new ValueUDIntArray(languageIds, 0x20)); // Empty for all languanges? Otherwise e.g. 1031 for de-de or what you need. - asrefsobj.AddAttribute(Ids.AlarmSubscriptionRef_SendAlarmTexts_Rid, new ValueBool(true)); - - asrefsobj.AddRelation(Ids.AlarmSubscriptionRef_itsAlarmSubsystem, 0x00000008); - subsobj.AddObject(asrefsobj); - // Build the request object - var createObjReq = new CreateObjectRequest(ProtocolVersion.V2, 0, true); - createObjReq.TransportFlags = 0x34; - createObjReq.RequestId = SessionId2; - createObjReq.RequestValue = new ValueUDInt(0); - createObjReq.SetRequestObject(subsobj); - - // Send it - 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("AlarmSubscription - Create: CreateObjectResponse with Error!"); - return S7Consts.errIsoInvalidPDU; - } - - if (createObjRes.ReturnValue == 0) - { - // Save the ObjectId, to modify the existing subscription - m_AlarmSubscriptionObjectId = createObjRes.ObjectIds[0]; - } - else - { - // If creating a subscription fails, the object is still created and should be deleted. - // At least deleting it, gives no error. - Console.WriteLine(String.Format("AlarmSubscription - Create: Failed with Returnvalue = 0x{0:X8}", createObjRes.ReturnValue)); - res = S7Consts.errCliInvalidParams; - } - - return res; - } - - //public int TestWaitForAlarmNotifications(int waitTimeout, int untilNumberOfAlarms, int alarmTextsLanguageId, out List notifications) - //{ - // int res = 0; - // short creditLimitStep = 5; - - // for (int i = 1; i <= untilNumberOfAlarms; i++) - // { - // Console.WriteLine(Environment.NewLine + "WaitForAlarmNotifications(): *** Loop #" + i.ToString() + " ***"); - // m_LastError = 0; - // WaitForNewS7plusReceived(waitTimeout); - // if (m_LastError != 0) - // { - // return m_LastError; - // } - - // var noti = Notification.DeserializeFromPdu(m_ReceivedPDU); - // if (noti == null) - // { - // Console.WriteLine("Notification == null!"); - // return S7Consts.errIsoInvalidPDU; - // } - // else - // { - // Console.Write("Notification: CreditTick=" + noti.NotificationCreditTick + " SequenceNumber=" + noti.NotificationSequenceNumber); - // Console.WriteLine(String.Format(" PLC-Timestamp={0}.{1:D03}", noti.Add1Timestamp.ToString(), noti.Add1Timestamp.Millisecond)); - - // var dai = AlarmsDai.FromNotificationObject(noti.P2Objects[0], alarmTextsLanguageId); - // Console.WriteLine(dai.ToString()); - // if (noti.NotificationCreditTick >= m_AlarmNextCreditLimit - 1) // Set new limit one tick before it expires, to get a constant flow of data - // { - // // CreditTick in Notification is only one byte - // m_AlarmNextCreditLimit = (short)((m_AlarmNextCreditLimit + creditLimitStep) % 255); - // Console.WriteLine("--> Credit limit of " + noti.NotificationCreditTick + " reached. SetCreditLimit to " + m_AlarmNextCreditLimit.ToString()); - // SubscriptionSetCreditLimit(m_AlarmNextCreditLimit); - // } - // } - // } - // return res; +#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 S7CommPlusDriver.Alarming; +using System; +using System.Collections.Generic; + +namespace S7CommPlusDriver +{ + public partial class S7CommPlusConnection + { + // ************************************************* + // * IMPORTANT! * + // * This is basically a test for Alarming, * + // * how to use it, and how to later integrate * + // * this into the complete library! * + // ************************************************* + // + // Example code for testing: + // CultureInfo ci = new CultureInfo("en-US"); + // conn.AlarmSubscriptionCreate(); + // conn.TestWaitForAlarmNotifications(20000, 3, ci.LCID); + // conn.AlarmSubscriptionDelete(); + + uint m_AlarmSubscriptionRelationId = 0x7fffc001; // TODO! Unknown value! See also Subscription.cs + uint m_AlarmSubscriptionRefRelationId = 0x51010001; // TODO! Unknown value! + short m_AlarmNextCreditLimit; + uint m_AlarmSubscriptionObjectId; + + public int AlarmSubscriptionCreate(uint[] languageIds) + { + int res; + PObject subsobj = new PObject(); + subsobj.ClassId = Ids.ClassSubscription; + subsobj.RelationId = m_AlarmSubscriptionRelationId; + subsobj.AddAttribute(Ids.ObjectVariableTypeName, new ValueWString("Subscription_" + m_AlarmSubscriptionRelationId.ToString())); + subsobj.AddAttribute(Ids.SubscriptionFunctionClassId, new ValueUSInt(2)); + subsobj.AddAttribute(Ids.SubscriptionMissedSendings, new ValueUInt(0)); + subsobj.AddAttribute(Ids.SubscriptionSubsystemError, new ValueLInt(0)); + subsobj.AddAttribute(Ids.SubscriptionRouteMode, new ValueUSInt(2)); // TODO Unknown + subsobj.AddAttribute(Ids.SubscriptionActive, new ValueBool(true)); + subsobj.AddAttribute(Ids.SubscriptionReferenceList, new ValueUDIntArray(new uint[3] { 0x80010000, 0, 0 }, 0x20)); // 0x20 = Adressarray + subsobj.AddAttribute(Ids.SubscriptionCycleTime, new ValueUDInt(0)); + subsobj.AddAttribute(Ids.SubscriptionDelayTime, new ValueUDInt(0)); + subsobj.AddAttribute(Ids.SubscriptionDisabled, new ValueUSInt(0)); + subsobj.AddAttribute(Ids.SubscriptionCount, new ValueUSInt(0)); + m_AlarmNextCreditLimit = 10; + subsobj.AddAttribute(Ids.SubscriptionCreditLimit, new ValueInt(m_AlarmNextCreditLimit)); // -1=unlimited, 255 = max + subsobj.AddAttribute(Ids.SubscriptionTicks, new ValueUInt(65535)); // 65535 + // 1055 = Unknown -> is working without setting this. Maybe default attribute is zero. + //subsobj.AddAttribute(1055, new ValueUSInt(0)); + PObject asrefsobj = new PObject(); + asrefsobj.ClassId = Ids.AlarmSubscriptionRef_Class_Rid; + asrefsobj.RelationId = m_AlarmSubscriptionRefRelationId; + asrefsobj.AddAttribute(Ids.ObjectVariableTypeName, new ValueWString("S7pDriver_Alarming")); + asrefsobj.AddAttribute(Ids.SubscriptionReferenceTriggerAndTransmitMode, new ValueUSInt(3)); + asrefsobj.AddAttribute(Ids.AlarmSubscriptionRef_AlarmDomain, new ValueUIntArray(new ushort[10] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 0x10)); + // Also variant to set explicit the alarm domain as filter, for example: + // {1, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272} + // Possibly 65535 is "all". + asrefsobj.AddAttribute(Ids.AlarmSubscriptionRef_AlarmDomain2, new ValueUIntArray(new ushort[1] { 65535 }, 0x20)); // 0x20 = Adressarray + // OPTION: + // Send text informations with the message, we don't need to browse them in advance. + asrefsobj.AddAttribute(Ids.AlarmSubscriptionRef_AlarmTextLanguages_Rid, new ValueUDIntArray(languageIds, 0x20)); // Empty for all languanges? Otherwise e.g. 1031 for de-de or what you need. + asrefsobj.AddAttribute(Ids.AlarmSubscriptionRef_SendAlarmTexts_Rid, new ValueBool(true)); + + asrefsobj.AddRelation(Ids.AlarmSubscriptionRef_itsAlarmSubsystem, 0x00000008); + subsobj.AddObject(asrefsobj); + // Build the request object + var createObjReq = new CreateObjectRequest(ProtocolVersion.V2, 0, true); + createObjReq.TransportFlags = 0x34; + createObjReq.RequestId = SessionId2; + createObjReq.RequestValue = new ValueUDInt(0); + createObjReq.SetRequestObject(subsobj); + + // Send it + 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("AlarmSubscription - Create: CreateObjectResponse with Error!"); + return S7Consts.errIsoInvalidPDU; + } + + if (createObjRes.ReturnValue == 0) + { + // Save the ObjectId, to modify the existing subscription + m_AlarmSubscriptionObjectId = createObjRes.ObjectIds[0]; + } + else + { + // If creating a subscription fails, the object is still created and should be deleted. + // At least deleting it, gives no error. + Console.WriteLine(String.Format("AlarmSubscription - Create: Failed with Returnvalue = 0x{0:X8}", createObjRes.ReturnValue)); + res = S7Consts.errCliInvalidParams; + } + + return res; + } + + //public int TestWaitForAlarmNotifications(int waitTimeout, int untilNumberOfAlarms, int alarmTextsLanguageId, out List notifications) + //{ + // int res = 0; + // short creditLimitStep = 5; + + // for (int i = 1; i <= untilNumberOfAlarms; i++) + // { + // Console.WriteLine(Environment.NewLine + "WaitForAlarmNotifications(): *** Loop #" + i.ToString() + " ***"); + // m_LastError = 0; + // WaitForNewS7plusReceived(waitTimeout); + // if (m_LastError != 0) + // { + // return m_LastError; + // } + + // var noti = Notification.DeserializeFromPdu(m_ReceivedPDU); + // if (noti == null) + // { + // Console.WriteLine("Notification == null!"); + // return S7Consts.errIsoInvalidPDU; + // } + // else + // { + // Console.Write("Notification: CreditTick=" + noti.NotificationCreditTick + " SequenceNumber=" + noti.NotificationSequenceNumber); + // Console.WriteLine(String.Format(" PLC-Timestamp={0}.{1:D03}", noti.Add1Timestamp.ToString(), noti.Add1Timestamp.Millisecond)); + + // var dai = AlarmsDai.FromNotificationObject(noti.P2Objects[0], alarmTextsLanguageId); + // Console.WriteLine(dai.ToString()); + // if (noti.NotificationCreditTick >= m_AlarmNextCreditLimit - 1) // Set new limit one tick before it expires, to get a constant flow of data + // { + // // CreditTick in Notification is only one byte + // m_AlarmNextCreditLimit = (short)((m_AlarmNextCreditLimit + creditLimitStep) % 255); + // Console.WriteLine("--> Credit limit of " + noti.NotificationCreditTick + " reached. SetCreditLimit to " + m_AlarmNextCreditLimit.ToString()); + // SubscriptionSetCreditLimit(m_AlarmNextCreditLimit); + // } + // } + // } + // return res; //} public int WaitForAlarmNotifications(int waitTimeout, out List notifications) @@ -193,15 +193,15 @@ public int WaitForAlarmNotifications(int waitTimeout, out List not } } return res; - } - - public int AlarmSubscriptionDelete() - { - int res; - m_AlarmSubscriptionObjectId = 0; - Console.WriteLine(String.Format("SubscriptionDelete: Calling DeleteObject for SessionId2={0:X8}", SessionId2)); - res = DeleteObject(SessionId2); - return res; - } - } -} + } + + public int AlarmSubscriptionDelete() + { + int res; + m_AlarmSubscriptionObjectId = 0; + Console.WriteLine(String.Format("SubscriptionDelete: Calling DeleteObject for SessionId2={0:X8}", SessionId2)); + res = DeleteObject(SessionId2); + return res; + } + } +} diff --git a/src/S7CommPlusDriver/Alarming/BrowseAlarms.cs b/src/S7CommPlusDriver/Alarming/BrowseAlarms.cs index a7f96b5..2f9be3b 100644 --- a/src/S7CommPlusDriver/Alarming/BrowseAlarms.cs +++ b/src/S7CommPlusDriver/Alarming/BrowseAlarms.cs @@ -413,10 +413,10 @@ public ulong GetCpuAlarmId() { return ((ulong)(RelationId) << 32) | ((ulong)(MultipleStai.Alid) << 16); } - + public uint RelationId; - public AlarmsMultipleStai MultipleStai; + public AlarmsMultipleStai MultipleStai; public AlarmsAlarmTexts AlText = new AlarmsAlarmTexts(); public int Deserialize(Stream buffer) @@ -432,7 +432,7 @@ public override string ToString() string s = ""; s += "" + Environment.NewLine; s += "" + GetCpuAlarmId().ToString() + "" + Environment.NewLine; - s += "" + RelationId.ToString() + Environment.NewLine +"" + Environment.NewLine; + s += "" + RelationId.ToString() + Environment.NewLine + "" + Environment.NewLine; s += "" + Environment.NewLine + MultipleStai.ToString() + "" + Environment.NewLine; s += "" + Environment.NewLine; s += "" + AlText.Infotext + "" + Environment.NewLine; diff --git a/src/S7CommPlusDriver/Net/S7Client.cs b/src/S7CommPlusDriver/Net/S7Client.cs index 28fa468..b6fcdf3 100644 --- a/src/S7CommPlusDriver/Net/S7Client.cs +++ b/src/S7CommPlusDriver/Net/S7Client.cs @@ -1,147 +1,147 @@ -#region License -/****************************************************************************** - * S7CommPlusDriver - * - * Based on Snap7 (Sharp7.cs) by Davide Nardella licensed under LGPL - * - /****************************************************************************/ -#endregion - -using OpenSsl; -using System; -using System.IO; -using System.Threading; - -namespace S7CommPlusDriver -{ - // Teilweise basierend auf Snap7 (Sharp7.cs) von Davide Nardella - // | Sharp7 is free software: you can redistribute it and/or modify | - // | it under the terms of the Lesser GNU General Public License as published by | - // | the Free Software Foundation, either version 3 of the License, or | - // | (at your option) any later version. | - public class S7Client : OpenSSLConnector.IConnectorCallback - { - //TODO: better API, maybe a Callback +#region License +/****************************************************************************** + * S7CommPlusDriver + * + * Based on Snap7 (Sharp7.cs) by Davide Nardella licensed under LGPL + * + /****************************************************************************/ +#endregion + +using OpenSsl; +using System; +using System.IO; +using System.Threading; + +namespace S7CommPlusDriver +{ + // Teilweise basierend auf Snap7 (Sharp7.cs) von Davide Nardella + // | Sharp7 is free software: you can redistribute it and/or modify | + // | it under the terms of the Lesser GNU General Public License as published by | + // | the Free Software Foundation, either version 3 of the License, or | + // | (at your option) any later version. | + 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 - - #region [S7 Telegrams] - - // ISO Connection Request telegram (contains also ISO Header and COTP Header) - byte[] ISO_CR = { - // TPKT (RFC1006 Header) - 0x03, // RFC 1006 ID (3) - 0x00, // Reserved, always 0 - 0x00, // High part of packet lenght (entire frame, payload and TPDU included) - 0x24, // Low part of packet lenght (entire frame, payload and TPDU included) - // COTP (ISO 8073 Header) - 0x1f, // PDU Size Length - 0xE0, // CR - Connection Request ID - 0x00, // Dst Reference HI - 0x00, // Dst Reference LO - 0x00, // Src Reference HI - 0x01, // Src Reference LO - 0x00, // Class + Options Flags - 0xC0, // PDU Max Length ID - 0x01, // PDU Max Length HI - 0x0A, // PDU Max Length LO - 0xC1, // Src TSAP Identifier - 0x02, // Src TSAP Length (2 bytes) - 0x01, // Src TSAP HI (will be overwritten) - 0x00, // Src TSAP LO (will be overwritten) - 0xC2, // Dst TSAP Identifier - 0x10, // Dst TSAP Length (16 bytes) - // Ab hier TSAP ID (String) - // SIMATIC-ROOT-HMI - }; - - // TPKT + ISO COTP Header (Connection Oriented Transport Protocol) - byte[] TPKT_ISO = { // 7 bytes - 0x03,0x00, - 0x00,0x1f, // Telegram Length (Data Size + 31 or 35) - 0x02,0xf0,0x80 // COTP (see above for info) - }; - - #endregion - - #region S7commPlus - - bool m_SslActive = false; - Thread m_runThread; - bool m_runThread_DoStop; - IntPtr m_ptr_ssl_method; - IntPtr m_ptr_ctx; - OpenSSLConnector m_sslconn; - - DateTime m_DateTimeStarted; - Native.SSL_CTX_keylog_cb_func m_keylog_cb; - - // OpenSSL möchte Daten auf den Socket aussenden. - public void WriteData(byte[] pData, int dataLength) - { - // SSL fordert Daten zum Absenden an - // TODO: Was ist, wenn SSL Daten verschicken möchte, die größer als eine TPDU sind? - // Bei großen Zertifikaten oder ähnlichem? Fragmentierung hier? - // Console.WriteLine("S7Client - OpenSSL WriteData: dataLength=" + dataLength); - byte[] sendData = new byte[dataLength]; - Array.Copy(pData, sendData, dataLength); - SendIsoPacket(sendData); - } - - // OpenSSL meldet fertige Daten (decrypted) zum einlesen - public void OnDataAvailable() - { - // Netzwerk meldet eintreffende Daten - byte[] buf = new byte[8192]; - int bytesRead = m_sslconn.Receive(ref buf, buf.Length); - // Console.WriteLine("S7Client - OpenSSL OnDataAvailable: bytesRead=" + bytesRead); - byte[] readData = new byte[bytesRead]; - Array.Copy(buf, readData, bytesRead); - OnDataReceived?.Invoke(readData, bytesRead); - } - - // OpenSSL Key Callback Funktion. Gibt die ausgehandelden privaten Schlüssel aus. Kann beispielsweise - // in eine Wireshark Aufzeichnung eingefügt werden um dort die TLS Kommunikation zu entschlüsseln. - public void SSL_CTX_keylog_cb(IntPtr ssl, string line) - { - string filename = "key_" + m_DateTimeStarted.ToString("yyyyMMdd_HHmmss") + ".log"; + #region [Constants and TypeDefs] + + public int _LastError = 0; + + #endregion + + #region [S7 Telegrams] + + // ISO Connection Request telegram (contains also ISO Header and COTP Header) + byte[] ISO_CR = { + // TPKT (RFC1006 Header) + 0x03, // RFC 1006 ID (3) + 0x00, // Reserved, always 0 + 0x00, // High part of packet lenght (entire frame, payload and TPDU included) + 0x24, // Low part of packet lenght (entire frame, payload and TPDU included) + // COTP (ISO 8073 Header) + 0x1f, // PDU Size Length + 0xE0, // CR - Connection Request ID + 0x00, // Dst Reference HI + 0x00, // Dst Reference LO + 0x00, // Src Reference HI + 0x01, // Src Reference LO + 0x00, // Class + Options Flags + 0xC0, // PDU Max Length ID + 0x01, // PDU Max Length HI + 0x0A, // PDU Max Length LO + 0xC1, // Src TSAP Identifier + 0x02, // Src TSAP Length (2 bytes) + 0x01, // Src TSAP HI (will be overwritten) + 0x00, // Src TSAP LO (will be overwritten) + 0xC2, // Dst TSAP Identifier + 0x10, // Dst TSAP Length (16 bytes) + // Ab hier TSAP ID (String) + // SIMATIC-ROOT-HMI + }; + + // TPKT + ISO COTP Header (Connection Oriented Transport Protocol) + byte[] TPKT_ISO = { // 7 bytes + 0x03,0x00, + 0x00,0x1f, // Telegram Length (Data Size + 31 or 35) + 0x02,0xf0,0x80 // COTP (see above for info) + }; + + #endregion + + #region S7commPlus + + bool m_SslActive = false; + Thread m_runThread; + bool m_runThread_DoStop; + IntPtr m_ptr_ssl_method; + IntPtr m_ptr_ctx; + OpenSSLConnector m_sslconn; + + DateTime m_DateTimeStarted; + Native.SSL_CTX_keylog_cb_func m_keylog_cb; + + // OpenSSL möchte Daten auf den Socket aussenden. + public void WriteData(byte[] pData, int dataLength) + { + // SSL fordert Daten zum Absenden an + // TODO: Was ist, wenn SSL Daten verschicken möchte, die größer als eine TPDU sind? + // Bei großen Zertifikaten oder ähnlichem? Fragmentierung hier? + // Console.WriteLine("S7Client - OpenSSL WriteData: dataLength=" + dataLength); + byte[] sendData = new byte[dataLength]; + Array.Copy(pData, sendData, dataLength); + SendIsoPacket(sendData); + } + + // OpenSSL meldet fertige Daten (decrypted) zum einlesen + public void OnDataAvailable() + { + // Netzwerk meldet eintreffende Daten + byte[] buf = new byte[8192]; + int bytesRead = m_sslconn.Receive(ref buf, buf.Length); + // Console.WriteLine("S7Client - OpenSSL OnDataAvailable: bytesRead=" + bytesRead); + byte[] readData = new byte[bytesRead]; + Array.Copy(buf, readData, bytesRead); + OnDataReceived?.Invoke(readData, bytesRead); + } + + // OpenSSL Key Callback Funktion. Gibt die ausgehandelden privaten Schlüssel aus. Kann beispielsweise + // in eine Wireshark Aufzeichnung eingefügt werden um dort die TLS Kommunikation zu entschlüsseln. + public void SSL_CTX_keylog_cb(IntPtr ssl, string line) + { + string filename = "key_" + m_DateTimeStarted.ToString("yyyyMMdd_HHmmss") + ".log"; if (WriteSslKeyPath != null) - filename = Path.Combine(WriteSslKeyPath, filename); - StreamWriter file = new StreamWriter(filename, append: true); - file.WriteLine(line); - file.Close(); - } - - // Startet OpenSSL und aktiviert ab jetzt TLS - public int SslActivate() - { - int ret; - try - { - ret = Native.OPENSSL_init_ssl(0, IntPtr.Zero); // returns 1 on success or 0 on error - if (ret != 1) - { - return S7Consts.errOpenSSL; - } - m_ptr_ssl_method = Native.ExpectNonNull(Native.TLS_client_method()); - m_ptr_ctx = Native.ExpectNonNull(Native.SSL_CTX_new(m_ptr_ssl_method)); - // TLS 1.3 forcieren, da wegen TLS on IsoOnTCP bekannt sein muss, um wie viele Bytes sich die verschlüsselten - // Daten verlängern um die Pakete auf S7CommPlus-Ebene entsprechend zu fragmentieren. - // Die Verlängerung geschieht z.B. durch Padding und HMAC. Bei TLS 1.3 existiert mit GCM kein Padding und verlängert sich immer - // um 16 Bytes. Da auch TLS_CHACHA20_POLY1305_SHA256 zu den TLS 1.3 CipherSuite zählt, explizit die anderen setzen. - Native.SSL_CTX_ctrl(m_ptr_ctx, Native.SSL_CTRL_SET_MIN_PROTO_VERSION, Native.TLS1_3_VERSION, IntPtr.Zero); - ret = Native.SSL_CTX_set_ciphersuites(m_ptr_ctx, "TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256"); - if (ret != 1) - { - return S7Consts.errOpenSSL; - } - m_sslconn = new OpenSSLConnector(m_ptr_ctx, this); + filename = Path.Combine(WriteSslKeyPath, filename); + StreamWriter file = new StreamWriter(filename, append: true); + file.WriteLine(line); + file.Close(); + } + + // Startet OpenSSL und aktiviert ab jetzt TLS + public int SslActivate() + { + int ret; + try + { + ret = Native.OPENSSL_init_ssl(0, IntPtr.Zero); // returns 1 on success or 0 on error + if (ret != 1) + { + return S7Consts.errOpenSSL; + } + m_ptr_ssl_method = Native.ExpectNonNull(Native.TLS_client_method()); + m_ptr_ctx = Native.ExpectNonNull(Native.SSL_CTX_new(m_ptr_ssl_method)); + // TLS 1.3 forcieren, da wegen TLS on IsoOnTCP bekannt sein muss, um wie viele Bytes sich die verschlüsselten + // Daten verlängern um die Pakete auf S7CommPlus-Ebene entsprechend zu fragmentieren. + // Die Verlängerung geschieht z.B. durch Padding und HMAC. Bei TLS 1.3 existiert mit GCM kein Padding und verlängert sich immer + // um 16 Bytes. Da auch TLS_CHACHA20_POLY1305_SHA256 zu den TLS 1.3 CipherSuite zählt, explizit die anderen setzen. + Native.SSL_CTX_ctrl(m_ptr_ctx, Native.SSL_CTRL_SET_MIN_PROTO_VERSION, Native.TLS1_3_VERSION, IntPtr.Zero); + ret = Native.SSL_CTX_set_ciphersuites(m_ptr_ctx, "TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256"); + if (ret != 1) + { + return S7Consts.errOpenSSL; + } + m_sslconn = new OpenSSLConnector(m_ptr_ctx, this); m_sslconn.ExpectConnect(); // Keylog callback setzen @@ -149,573 +149,573 @@ public int SslActivate() { 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; - } - return 0; - } - - // Deaktiviert TLS - public void SslDeactivate() - { - m_SslActive = false; - // TODO: Ist hier etwas zu OpenSSL-Ressourcen explizit freizugeben? - } - #endregion - - private void StartThread() - { - m_runThread_DoStop = false; - m_runThread = new Thread(RunThread); - m_runThread.Start(); - } - - // Der Task der kontinuierlich ausgeführt wird - private void RunThread() - { - int Length; - while (!m_runThread_DoStop) - { - // Versuchen zu lesen - _LastError = 0; - Length = RecvIsoPacket(); - // TODO: Hier nur den Payload zurückgeben - 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; - if (m_SslActive) - { - // Durch SSL eingelesene Daten an SSL weiterleiten - m_sslconn.ReadCompleted(Buffer, Size); - } else { - // Wenn etwas gelesen werden konnte, Client benachrichtigen - OnDataReceived?.Invoke(Buffer, Size); - } - } - } - } - - public _OnDataReceived OnDataReceived; - public delegate void _OnDataReceived(byte[] PDU, int len); - - #region [Internals] - - // Defaults - private static int ISOTCP = 102; // ISOTCP Port - private static int MinPduSizeToRequest = 240; - private static int MaxPduSizeToRequest = 960; - private static int DefaultTimeout = 2000; - private static int IsoHSize = 7; // TPKT+COTP Header Size - - // Properties - private int _PDULength = 0; - private int _PduSizeRequested = 480; - private int _PLCPort = ISOTCP; - private int _RecvTimeout = DefaultTimeout; - private int _SendTimeout = DefaultTimeout; - private int _ConnTimeout = DefaultTimeout; - - // Privates - private string IPAddress; - private byte LocalTSAP_HI; - private byte LocalTSAP_LO; - private byte[] RemoteTSAP_S; - private byte LastPDUType; - private byte[] PDU = new byte[2048]; - private MsgSocket Socket = null; - private int Time_ms = 0; - - private void CreateSocket() - { - try - { - Socket = new MsgSocket(); - Socket.ConnectTimeout = _ConnTimeout; - Socket.ReadTimeout = _RecvTimeout; - Socket.WriteTimeout = _SendTimeout; - } - catch - { - } - } - - private int TCPConnect() - { - if (_LastError == 0) - try - { - _LastError = Socket.Connect(IPAddress, _PLCPort); - } - catch - { - _LastError = S7Consts.errTCPConnectionFailed; - } - return _LastError; - } - - private void RecvPacket(byte[] Buffer, int Start, int Size) - { - if (Connected) - _LastError = Socket.Receive(Buffer, Start, Size); - else - _LastError = S7Consts.errTCPNotConnected; - } - - private void SendPacket(byte[] Buffer, int Len) - { - _LastError = Socket.Send(Buffer, Len); - } - - private void SendPacket(byte[] Buffer) - { - if (Connected) - SendPacket(Buffer, Buffer.Length); - else - _LastError = S7Consts.errTCPNotConnected; - } - - public void Send(byte[] Buffer) - { - if (m_SslActive) - { - m_sslconn.Write(Buffer, Buffer.Length); - } - else - { - SendIsoPacket(Buffer); - } - } - - private int 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)); - try - { - Array.Copy(Buffer, 0, PDU, TPKT_ISO.Length, Size); - } - catch - { - return S7Consts.errIsoInvalidPDU; - } - SendPacket(PDU, TPKT_ISO.Length + Size); - - return _LastError; - } - - 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 int RecvIsoPacket() - { - Boolean Done = false; - int Size = 0; - while ((_LastError == 0) && !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 - { - // 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; - } - - private int ISOConnect() - { - int Size; - byte[] isocon = new byte[ISO_CR.Length + RemoteTSAP_S.Length]; - ISO_CR[16] = LocalTSAP_HI; - ISO_CR[17] = LocalTSAP_LO; - - ISO_CR[3] = (byte)(20 + RemoteTSAP_S.Length); - ISO_CR[4] = (byte)(15 + RemoteTSAP_S.Length); - ISO_CR[19] = (byte)RemoteTSAP_S.Length; - - Array.Copy(ISO_CR, isocon, 20); - Array.Copy(RemoteTSAP_S, 0, isocon, 20, RemoteTSAP_S.Length); - - // Sends the connection request telegram - SendPacket(isocon); - if (_LastError == 0) - { - // 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; - } - } - return _LastError; - } - - public byte[] getOMSExporterSecret() - { - if (m_sslconn == null) return null; - return m_sslconn.getOMSExporterSecret(); - } - - #endregion - - #region [Class Control] - - public S7Client() - { - m_DateTimeStarted = DateTime.Now; - CreateSocket(); - } - - ~S7Client() - { - Disconnect(); - } - - public int Connect() - { - _LastError = 0; - Time_ms = 0; - int Elapsed = Environment.TickCount; - if (!Connected) - { - TCPConnect(); // First stage : TCP Connection - if (_LastError == 0) - { - ISOConnect(); // Second stage : ISOTCP (ISO 8073) Connection - if (_LastError == 0) - { - // _LastError = S7P_InitSSLRequest(); // Third stage : Init SSL Request - StartThread(); - } - } - } - if (_LastError != 0) - Disconnect(); - else - Time_ms = Environment.TickCount - Elapsed; - - return _LastError; - } - - public int SetConnectionParams(string Address, ushort LocalTSAP, byte[] RemoteTSAP) - { - int LocTSAP = LocalTSAP & 0x0000FFFF; - IPAddress = Address; - LocalTSAP_HI = (byte)(LocTSAP >> 8); - LocalTSAP_LO = (byte)(LocTSAP & 0x00FF); - - RemoteTSAP_S = new byte[RemoteTSAP.Length]; - Array.Copy(RemoteTSAP, RemoteTSAP_S, RemoteTSAP.Length); - - return 0; - } - - public int Disconnect() - { - m_runThread_DoStop = true; - m_runThread?.Join(); - - Socket.Close(); - - return 0; - } - - public int GetParam(Int32 ParamNumber, ref int Value) - { - int Result = 0; - switch (ParamNumber) - { - case S7Consts.p_u16_RemotePort: - { - Value = PLCPort; - break; - } - case S7Consts.p_i32_PingTimeout: - { - Value = ConnTimeout; - break; - } - case S7Consts.p_i32_SendTimeout: - { - Value = SendTimeout; - break; - } - case S7Consts.p_i32_RecvTimeout: - { - Value = RecvTimeout; - break; - } - case S7Consts.p_i32_PDURequest: - { - Value = PduSizeRequested; - break; - } - default: - { - Result = S7Consts.errCliInvalidParamNumber; - break; - } - } - return Result; - } - - // Set Properties for compatibility with Snap7.net.cs - public int SetParam(Int32 ParamNumber, ref int Value) - { - int Result = 0; - switch (ParamNumber) - { - case S7Consts.p_u16_RemotePort: - { - PLCPort = Value; - break; - } - case S7Consts.p_i32_PingTimeout: - { - ConnTimeout = Value; - break; - } - case S7Consts.p_i32_SendTimeout: - { - SendTimeout = Value; - break; - } - case S7Consts.p_i32_RecvTimeout: - { - RecvTimeout = Value; - break; - } - case S7Consts.p_i32_PDURequest: - { - PduSizeRequested = Value; - break; - } - default: - { - Result = S7Consts.errCliInvalidParamNumber; - break; - } - } - return Result; - } - - #endregion - - #region [Info Functions / Properties] - - 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.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.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"; - case S7Consts.errCliInvalidParams: return "CLI : invalid param(s) supplied"; - case S7Consts.errCliJobPending: return "CLI : Job pending"; - case S7Consts.errCliTooManyItems: return "CLI : too may items (>20) in multi read/write"; - case S7Consts.errCliInvalidWordLen: return "CLI : invalid WordLength"; - case S7Consts.errCliPartialDataWritten: return "CLI : Partial data written"; - case S7Consts.errCliSizeOverPDU: return "CPU : total data exceeds the PDU size"; - case S7Consts.errCliInvalidPlcAnswer: return "CLI : invalid CPU answer"; - case S7Consts.errCliAddressOutOfRange: return "CPU : Address out of range"; - case S7Consts.errCliInvalidTransportSize: return "CPU : Invalid Transport size"; - case S7Consts.errCliWriteDataSizeMismatch: return "CPU : Data size mismatch"; - case S7Consts.errCliItemNotAvailable: return "CPU : Item not available"; - case S7Consts.errCliInvalidValue: return "CPU : Invalid value supplied"; - case S7Consts.errCliCannotStartPLC: return "CPU : Cannot start PLC"; - case S7Consts.errCliAlreadyRun: return "CPU : PLC already RUN"; - case S7Consts.errCliCannotStopPLC: return "CPU : Cannot stop PLC"; - case S7Consts.errCliCannotCopyRamToRom: return "CPU : Cannot copy RAM to ROM"; - case S7Consts.errCliCannotCompress: return "CPU : Cannot compress"; - case S7Consts.errCliAlreadyStop: return "CPU : PLC already STOP"; - case S7Consts.errCliFunNotAvailable: return "CPU : Function not available"; - case S7Consts.errCliUploadSequenceFailed: return "CPU : Upload sequence failed"; - case S7Consts.errCliInvalidDataSizeRecvd: return "CLI : Invalid data size received"; - case S7Consts.errCliInvalidBlockType: return "CLI : Invalid block type"; - case S7Consts.errCliInvalidBlockNumber: return "CLI : Invalid block number"; - case S7Consts.errCliInvalidBlockSize: return "CLI : Invalid block size"; - case S7Consts.errCliNeedPassword: return "CPU : Function not authorized for current protection level"; - case S7Consts.errCliInvalidPassword: return "CPU : Invalid password"; - case S7Consts.errCliAccessDenied: return "CPU : Access denied"; - case S7Consts.errCliNoPasswordToSetOrClear: return "CPU : No password to set or clear"; - case S7Consts.errCliJobTimeout: return "CLI : Job Timeout"; - case S7Consts.errCliFunctionRefused: return "CLI : function refused by CPU (Unknown error)"; - case S7Consts.errCliPartialDataRead: return "CLI : Partial data read"; - case S7Consts.errCliBufferTooSmall: return "CLI : The buffer supplied is too small to accomplish the operation"; - case S7Consts.errCliDestroying: return "CLI : Cannot perform (destroying)"; - case S7Consts.errCliInvalidParamNumber: return "CLI : Invalid Param Number"; - case S7Consts.errCliCannotChangeParam: return "CLI : Cannot change this param now"; - case S7Consts.errCliFunctionNotImplemented: return "CLI : Function not implemented"; - 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() - { - return _PduSizeRequested; - } - - public int NegotiatedPduLength() - { - return _PDULength; - } - - public int ExecTime() - { - return Time_ms; - } - - public int ExecutionTime - { - get - { - return Time_ms; - } - } - - public int PduSizeNegotiated - { - get - { - return _PDULength; - } - } - - public int PduSizeRequested - { - get - { - return _PduSizeRequested; - } - set - { - if (value < MinPduSizeToRequest) - value = MinPduSizeToRequest; - if (value > MaxPduSizeToRequest) - value = MaxPduSizeToRequest; - _PduSizeRequested = value; - } - } - - public int PLCPort - { - get - { - return _PLCPort; - } - set - { - _PLCPort = value; - } - } - - public int ConnTimeout - { - get - { - return _ConnTimeout; - } - set - { - _ConnTimeout = value; - } - } - - public int RecvTimeout - { - get - { - return _RecvTimeout; - } - set - { - _RecvTimeout = value; - } - } - - public int SendTimeout - { - get - { - return _SendTimeout; - } - set - { - _SendTimeout = value; - } - } - - public bool Connected - { - get - { - return (Socket != null) && (Socket.Connected); - } - } - #endregion - } -} + } + + m_SslActive = true; + } + catch + { + return S7Consts.errOpenSSL; + } + return 0; + } + + // Deaktiviert TLS + public void SslDeactivate() + { + m_SslActive = false; + // TODO: Ist hier etwas zu OpenSSL-Ressourcen explizit freizugeben? + } + #endregion + + private void StartThread() + { + m_runThread_DoStop = false; + m_runThread = new Thread(RunThread); + m_runThread.Start(); + } + + // Der Task der kontinuierlich ausgeführt wird + private void RunThread() + { + int Length; + while (!m_runThread_DoStop) + { + // Versuchen zu lesen + _LastError = 0; + Length = RecvIsoPacket(); + // TODO: Hier nur den Payload zurückgeben + 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; + if (m_SslActive) + { + // Durch SSL eingelesene Daten an SSL weiterleiten + m_sslconn.ReadCompleted(Buffer, Size); + } else { + // Wenn etwas gelesen werden konnte, Client benachrichtigen + OnDataReceived?.Invoke(Buffer, Size); + } + } + } + } + + public _OnDataReceived OnDataReceived; + public delegate void _OnDataReceived(byte[] PDU, int len); + + #region [Internals] + + // Defaults + private static int ISOTCP = 102; // ISOTCP Port + private static int MinPduSizeToRequest = 240; + private static int MaxPduSizeToRequest = 960; + private static int DefaultTimeout = 2000; + private static int IsoHSize = 7; // TPKT+COTP Header Size + + // Properties + private int _PDULength = 0; + private int _PduSizeRequested = 480; + private int _PLCPort = ISOTCP; + private int _RecvTimeout = DefaultTimeout; + private int _SendTimeout = DefaultTimeout; + private int _ConnTimeout = DefaultTimeout; + + // Privates + private string IPAddress; + private byte LocalTSAP_HI; + private byte LocalTSAP_LO; + private byte[] RemoteTSAP_S; + private byte LastPDUType; + private byte[] PDU = new byte[2048]; + private MsgSocket Socket = null; + private int Time_ms = 0; + + private void CreateSocket() + { + try + { + Socket = new MsgSocket(); + Socket.ConnectTimeout = _ConnTimeout; + Socket.ReadTimeout = _RecvTimeout; + Socket.WriteTimeout = _SendTimeout; + } + catch + { + } + } + + private int TCPConnect() + { + if (_LastError == 0) + try + { + _LastError = Socket.Connect(IPAddress, _PLCPort); + } + catch + { + _LastError = S7Consts.errTCPConnectionFailed; + } + return _LastError; + } + + private void RecvPacket(byte[] Buffer, int Start, int Size) + { + if (Connected) + _LastError = Socket.Receive(Buffer, Start, Size); + else + _LastError = S7Consts.errTCPNotConnected; + } + + private void SendPacket(byte[] Buffer, int Len) + { + _LastError = Socket.Send(Buffer, Len); + } + + private void SendPacket(byte[] Buffer) + { + if (Connected) + SendPacket(Buffer, Buffer.Length); + else + _LastError = S7Consts.errTCPNotConnected; + } + + public void Send(byte[] Buffer) + { + if (m_SslActive) + { + m_sslconn.Write(Buffer, Buffer.Length); + } + else + { + SendIsoPacket(Buffer); + } + } + + private int 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)); + try + { + Array.Copy(Buffer, 0, PDU, TPKT_ISO.Length, Size); + } + catch + { + return S7Consts.errIsoInvalidPDU; + } + SendPacket(PDU, TPKT_ISO.Length + Size); + + return _LastError; + } + + 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 int RecvIsoPacket() + { + Boolean Done = false; + int Size = 0; + while ((_LastError == 0) && !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 + { + // 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; + } + + private int ISOConnect() + { + int Size; + byte[] isocon = new byte[ISO_CR.Length + RemoteTSAP_S.Length]; + ISO_CR[16] = LocalTSAP_HI; + ISO_CR[17] = LocalTSAP_LO; + + ISO_CR[3] = (byte)(20 + RemoteTSAP_S.Length); + ISO_CR[4] = (byte)(15 + RemoteTSAP_S.Length); + ISO_CR[19] = (byte)RemoteTSAP_S.Length; + + Array.Copy(ISO_CR, isocon, 20); + Array.Copy(RemoteTSAP_S, 0, isocon, 20, RemoteTSAP_S.Length); + + // Sends the connection request telegram + SendPacket(isocon); + if (_LastError == 0) + { + // 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; + } + } + return _LastError; + } + + public byte[] getOMSExporterSecret() + { + if (m_sslconn == null) return null; + return m_sslconn.getOMSExporterSecret(); + } + + #endregion + + #region [Class Control] + + public S7Client() + { + m_DateTimeStarted = DateTime.Now; + CreateSocket(); + } + + ~S7Client() + { + Disconnect(); + } + + public int Connect() + { + _LastError = 0; + Time_ms = 0; + int Elapsed = Environment.TickCount; + if (!Connected) + { + TCPConnect(); // First stage : TCP Connection + if (_LastError == 0) + { + ISOConnect(); // Second stage : ISOTCP (ISO 8073) Connection + if (_LastError == 0) + { + // _LastError = S7P_InitSSLRequest(); // Third stage : Init SSL Request + StartThread(); + } + } + } + if (_LastError != 0) + Disconnect(); + else + Time_ms = Environment.TickCount - Elapsed; + + return _LastError; + } + + public int SetConnectionParams(string Address, ushort LocalTSAP, byte[] RemoteTSAP) + { + int LocTSAP = LocalTSAP & 0x0000FFFF; + IPAddress = Address; + LocalTSAP_HI = (byte)(LocTSAP >> 8); + LocalTSAP_LO = (byte)(LocTSAP & 0x00FF); + + RemoteTSAP_S = new byte[RemoteTSAP.Length]; + Array.Copy(RemoteTSAP, RemoteTSAP_S, RemoteTSAP.Length); + + return 0; + } + + public int Disconnect() + { + m_runThread_DoStop = true; + m_runThread?.Join(); + + Socket.Close(); + + return 0; + } + + public int GetParam(Int32 ParamNumber, ref int Value) + { + int Result = 0; + switch (ParamNumber) + { + case S7Consts.p_u16_RemotePort: + { + Value = PLCPort; + break; + } + case S7Consts.p_i32_PingTimeout: + { + Value = ConnTimeout; + break; + } + case S7Consts.p_i32_SendTimeout: + { + Value = SendTimeout; + break; + } + case S7Consts.p_i32_RecvTimeout: + { + Value = RecvTimeout; + break; + } + case S7Consts.p_i32_PDURequest: + { + Value = PduSizeRequested; + break; + } + default: + { + Result = S7Consts.errCliInvalidParamNumber; + break; + } + } + return Result; + } + + // Set Properties for compatibility with Snap7.net.cs + public int SetParam(Int32 ParamNumber, ref int Value) + { + int Result = 0; + switch (ParamNumber) + { + case S7Consts.p_u16_RemotePort: + { + PLCPort = Value; + break; + } + case S7Consts.p_i32_PingTimeout: + { + ConnTimeout = Value; + break; + } + case S7Consts.p_i32_SendTimeout: + { + SendTimeout = Value; + break; + } + case S7Consts.p_i32_RecvTimeout: + { + RecvTimeout = Value; + break; + } + case S7Consts.p_i32_PDURequest: + { + PduSizeRequested = Value; + break; + } + default: + { + Result = S7Consts.errCliInvalidParamNumber; + break; + } + } + return Result; + } + + #endregion + + #region [Info Functions / Properties] + + 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.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.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"; + case S7Consts.errCliInvalidParams: return "CLI : invalid param(s) supplied"; + case S7Consts.errCliJobPending: return "CLI : Job pending"; + case S7Consts.errCliTooManyItems: return "CLI : too may items (>20) in multi read/write"; + case S7Consts.errCliInvalidWordLen: return "CLI : invalid WordLength"; + case S7Consts.errCliPartialDataWritten: return "CLI : Partial data written"; + case S7Consts.errCliSizeOverPDU: return "CPU : total data exceeds the PDU size"; + case S7Consts.errCliInvalidPlcAnswer: return "CLI : invalid CPU answer"; + case S7Consts.errCliAddressOutOfRange: return "CPU : Address out of range"; + case S7Consts.errCliInvalidTransportSize: return "CPU : Invalid Transport size"; + case S7Consts.errCliWriteDataSizeMismatch: return "CPU : Data size mismatch"; + case S7Consts.errCliItemNotAvailable: return "CPU : Item not available"; + case S7Consts.errCliInvalidValue: return "CPU : Invalid value supplied"; + case S7Consts.errCliCannotStartPLC: return "CPU : Cannot start PLC"; + case S7Consts.errCliAlreadyRun: return "CPU : PLC already RUN"; + case S7Consts.errCliCannotStopPLC: return "CPU : Cannot stop PLC"; + case S7Consts.errCliCannotCopyRamToRom: return "CPU : Cannot copy RAM to ROM"; + case S7Consts.errCliCannotCompress: return "CPU : Cannot compress"; + case S7Consts.errCliAlreadyStop: return "CPU : PLC already STOP"; + case S7Consts.errCliFunNotAvailable: return "CPU : Function not available"; + case S7Consts.errCliUploadSequenceFailed: return "CPU : Upload sequence failed"; + case S7Consts.errCliInvalidDataSizeRecvd: return "CLI : Invalid data size received"; + case S7Consts.errCliInvalidBlockType: return "CLI : Invalid block type"; + case S7Consts.errCliInvalidBlockNumber: return "CLI : Invalid block number"; + case S7Consts.errCliInvalidBlockSize: return "CLI : Invalid block size"; + case S7Consts.errCliNeedPassword: return "CPU : Function not authorized for current protection level"; + case S7Consts.errCliInvalidPassword: return "CPU : Invalid password"; + case S7Consts.errCliAccessDenied: return "CPU : Access denied"; + case S7Consts.errCliNoPasswordToSetOrClear: return "CPU : No password to set or clear"; + case S7Consts.errCliJobTimeout: return "CLI : Job Timeout"; + case S7Consts.errCliFunctionRefused: return "CLI : function refused by CPU (Unknown error)"; + case S7Consts.errCliPartialDataRead: return "CLI : Partial data read"; + case S7Consts.errCliBufferTooSmall: return "CLI : The buffer supplied is too small to accomplish the operation"; + case S7Consts.errCliDestroying: return "CLI : Cannot perform (destroying)"; + case S7Consts.errCliInvalidParamNumber: return "CLI : Invalid Param Number"; + case S7Consts.errCliCannotChangeParam: return "CLI : Cannot change this param now"; + case S7Consts.errCliFunctionNotImplemented: return "CLI : Function not implemented"; + 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() + { + return _PduSizeRequested; + } + + public int NegotiatedPduLength() + { + return _PDULength; + } + + public int ExecTime() + { + return Time_ms; + } + + public int ExecutionTime + { + get + { + return Time_ms; + } + } + + public int PduSizeNegotiated + { + get + { + return _PDULength; + } + } + + public int PduSizeRequested + { + get + { + return _PduSizeRequested; + } + set + { + if (value < MinPduSizeToRequest) + value = MinPduSizeToRequest; + if (value > MaxPduSizeToRequest) + value = MaxPduSizeToRequest; + _PduSizeRequested = value; + } + } + + public int PLCPort + { + get + { + return _PLCPort; + } + set + { + _PLCPort = value; + } + } + + public int ConnTimeout + { + get + { + return _ConnTimeout; + } + set + { + _ConnTimeout = value; + } + } + + public int RecvTimeout + { + get + { + return _RecvTimeout; + } + set + { + _RecvTimeout = value; + } + } + + public int SendTimeout + { + get + { + return _SendTimeout; + } + set + { + _SendTimeout = value; + } + } + + public bool Connected + { + get + { + return (Socket != null) && (Socket.Connected); + } + } + #endregion + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusConnection.cs b/src/S7CommPlusDriver/S7CommPlusConnection.cs index 853bfc8..c72fe89 100644 --- a/src/S7CommPlusDriver/S7CommPlusConnection.cs +++ b/src/S7CommPlusDriver/S7CommPlusConnection.cs @@ -1,1434 +1,1436 @@ -#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); - 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 -} +#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); + 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 +} From 3e4a79a7a258a18dbb50f32ada79e62df6f7135a Mon Sep 17 00:00:00 2001 From: Michal Zukiewicz Date: Fri, 22 May 2026 14:12:42 +0200 Subject: [PATCH 13/13] Added return value to Disconnect() --- src/S7CommPlusDriver/S7CommPlusConnection.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/S7CommPlusDriver/S7CommPlusConnection.cs b/src/S7CommPlusDriver/S7CommPlusConnection.cs index c72fe89..341323e 100644 --- a/src/S7CommPlusDriver/S7CommPlusConnection.cs +++ b/src/S7CommPlusDriver/S7CommPlusConnection.cs @@ -533,10 +533,11 @@ public int Connect(string address, string password = "", string username = "", i return 0; } - public void Disconnect() + public int Disconnect() { - DeleteObject(m_SessionId); + int res = DeleteObject(m_SessionId); m_client.Disconnect(); + return res; } ///